From e9a1c30462b7363a28f6d8c0a7aa88380a2f0f36 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 10 Dec 2024 16:02:00 +0100 Subject: [PATCH 01/18] knock requests : expose api through Room --- .../libraries/matrix/api/room/MatrixRoom.kt | 6 +++ .../matrix/api/room/knock/KnockRequest.kt | 26 +++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 13 ++++++ .../impl/room/knock/RustKnockRequest.kt | 38 ++++++++++++++++ .../matrix/test/room/FakeMatrixRoom.kt | 8 ++++ .../test/room/knock/FakeKnockRequest.kt | 43 +++++++++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt create mode 100644 libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 840308af23..82ea24e7ff 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.media.MediaUploadHandler import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.poll.PollKind import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange @@ -56,6 +57,11 @@ interface MatrixRoom : Closeable { val roomTypingMembersFlow: Flow> val identityStateChangesFlow: Flow> + /** + * The current knock requests in the room as a Flow. + */ + val knockRequestsFlow: Flow> + /** * A one-to-one is a room with exactly 2 members. * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/#default-underride-rules). diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt new file mode 100644 index 0000000000..f5b25b5f2c --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt @@ -0,0 +1,26 @@ +/* + * 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.libraries.matrix.api.room.knock + +import io.element.android.libraries.matrix.api.core.UserId + +interface KnockRequest { + val userId: UserId + val displayName: String? + val avatarUrl: String? + val reason: String? + val timestamp: Long? + + suspend fun accept(): Result + + suspend fun decline(reason: String?): Result + + suspend fun declineAndBan(reason: String?): Result + + suspend fun markAsSeen(): Result +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c84a37cdc3..44f588bd83 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange @@ -50,6 +51,7 @@ import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings import io.element.android.libraries.matrix.impl.core.RustSendHandle import io.element.android.libraries.matrix.impl.mapper.map import io.element.android.libraries.matrix.impl.room.draft.into +import io.element.android.libraries.matrix.impl.room.knock.RustKnockRequest import io.element.android.libraries.matrix.impl.room.member.RoomMemberListFetcher import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import io.element.android.libraries.matrix.impl.room.powerlevels.RoomPowerLevelsMapper @@ -76,6 +78,8 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener +import org.matrix.rustcomponents.sdk.JoinRequest +import org.matrix.rustcomponents.sdk.RequestsToJoinListener import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem @@ -157,6 +161,15 @@ class RustMatrixRoom( }) } + override val knockRequestsFlow: Flow> = mxCallbackFlow { + innerRoom.subscribeToJoinRequests(object : RequestsToJoinListener { + override fun call(joinRequests: List) { + val knockRequests = joinRequests.map { RustKnockRequest(it) } + channel.trySend(knockRequests) + } + }) + } + // Create a dispatcher for all room methods... private val roomDispatcher = coroutineDispatchers.io.limitedParallelism(32) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt new file mode 100644 index 0000000000..a3d133d599 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt @@ -0,0 +1,38 @@ +/* + * 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.libraries.matrix.impl.room.knock + +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.knock.KnockRequest +import org.matrix.rustcomponents.sdk.JoinRequest + +class RustKnockRequest( + private val inner: JoinRequest, +) : KnockRequest { + override val userId: UserId = UserId(inner.userId) + override val displayName: String? = inner.displayName + override val avatarUrl: String? = inner.avatarUrl + override val reason: String? = inner.reason + override val timestamp: Long? = inner.timestamp?.toLong() + + override suspend fun accept(): Result = runCatching { + inner.actions.accept() + } + + override suspend fun decline(reason: String?): Result = runCatching { + inner.actions.decline(reason) + } + + override suspend fun declineAndBan(reason: String?): Result = runCatching { + inner.actions.declineAndBan(reason) + } + + override suspend fun markAsSeen(): Result = runCatching { + inner.actions.markAsSeen() + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 41d913b89c..cfb73e267e 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -33,6 +33,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.draft.ComposerDraft +import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.api.room.location.AssetType import io.element.android.libraries.matrix.api.room.powerlevels.MatrixRoomPowerLevels import io.element.android.libraries.matrix.api.room.powerlevels.UserRoleChange @@ -163,6 +164,13 @@ class FakeMatrixRoom( _identityStateChangesFlow.tryEmit(identityStateChanges) } + private val _knockRequestsFlow: MutableSharedFlow> = MutableSharedFlow(replay = 1) + override val knockRequestsFlow: Flow> = _knockRequestsFlow + + fun emitKnockRequests(knockRequests: List) { + _knockRequestsFlow.tryEmit(knockRequests) + } + override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) override val roomNotificationSettingsStateFlow: MutableStateFlow = diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt new file mode 100644 index 0000000000..fad12e6bb6 --- /dev/null +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt @@ -0,0 +1,43 @@ +/* + * 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.libraries.matrix.test.room.knock + +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.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.tests.testutils.lambda.lambdaError + +class FakeKnockRequest( + override val userId: UserId = A_USER_ID, + override val displayName: String? = A_USER_NAME, + override val avatarUrl: String? = AN_AVATAR_URL, + override val reason: String? = null, + override val timestamp: Long? = null, + val acceptLambda: () -> Result = { lambdaError() }, + val declineLambda: (String?) -> Result = { lambdaError() }, + val declineAndBanLambda: (String?) -> Result = { lambdaError() }, + val markAsSeenLambda: () -> Result = { lambdaError() }, +) : KnockRequest { + override suspend fun accept(): Result { + return acceptLambda() + } + + override suspend fun decline(reason: String?): Result { + return declineLambda(reason) + } + + override suspend fun declineAndBan(reason: String?): Result { + return declineAndBanLambda(reason) + } + + override suspend fun markAsSeen(): Result { + return markAsSeenLambda() + } +} From dfb5362394b57f8b2667ae5d6933328c516d58e7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 11 Dec 2024 18:02:35 +0100 Subject: [PATCH 02/18] knock requests : branch the api in presenters --- .../knockrequests/impl/KnockRequest.kt | 45 ------ .../banner/KnockRequestsBannerPresenter.kt | 74 ++++++++-- .../impl/banner/KnockRequestsBannerState.kt | 8 +- .../KnockRequestsBannerStateProvider.kt | 16 +-- .../impl/banner/KnockRequestsBannerView.kt | 114 +++++++++------ .../impl/data/KnockRequestFixture.kt | 27 ++++ .../impl/data/KnockRequestPresentable.kt | 36 +++++ .../impl/data/KnockRequestWrapper.kt | 34 +++++ .../impl/data/KnockRequestsService.kt | 135 ++++++++++++++++++ .../impl/list/KnockRequestsListEvents.kt | 12 +- .../impl/list/KnockRequestsListPresenter.kt | 83 ++++++++--- .../impl/list/KnockRequestsListState.kt | 21 +-- .../list/KnockRequestsListStateProvider.kt | 26 +++- .../impl/list/KnockRequestsListView.kt | 113 ++++++++------- .../libraries/matrix/api/room/MatrixRoom.kt | 3 + .../matrix/api/room/knock/KnockRequest.kt | 3 + .../matrix/impl/room/RustMatrixRoom.kt | 6 +- .../impl/room/knock/RustKnockRequest.kt | 3 + .../matrix/test/room/FakeMatrixRoom.kt | 3 + 19 files changed, 555 insertions(+), 207 deletions(-) delete mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/KnockRequest.kt create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/KnockRequest.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/KnockRequest.kt deleted file mode 100644 index 1014d13e58..0000000000 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/KnockRequest.kt +++ /dev/null @@ -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, -) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt index a61236bbb0..c95fca6a5f 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt @@ -8,35 +8,89 @@ package io.element.android.features.knockrequests.impl.banner 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.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import io.element.android.libraries.architecture.AsyncAction +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable +import io.element.android.features.knockrequests.impl.data.KnockRequestsService 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 -class KnockRequestsBannerPresenter @Inject constructor() : Presenter { +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 { @Composable 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) { when (event) { - is KnockRequestsBannerEvents.AcceptSingleRequest -> Unit + is KnockRequestsBannerEvents.AcceptSingleRequest -> { + appCoroutineScope.acceptSingleKnockRequest( + knockRequests = knockRequests, + displayAcceptError = showAcceptError, + ) + } is KnockRequestsBannerEvents.Dismiss -> { - shouldShowBanner = false + appCoroutineScope.launch { + knockRequestsService.markAllKnockRequestsAsSeen() + } } } } return KnockRequestsBannerState( - knockRequests = persistentListOf(), - acceptAction = AsyncAction.Uninitialized, - canAccept = false, + knockRequests = knockRequests, + displayAcceptError = showAcceptError.value, + canAccept = canAccept, isVisible = shouldShowBanner, eventSink = ::handleEvents, ) } + + private fun CoroutineScope.acceptSingleKnockRequest( + knockRequests: List, + displayAcceptError: MutableState, + ) = 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 + } + } + } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt index e65cd2fb26..9d53181e82 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt @@ -10,17 +10,15 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import io.element.android.features.knockrequests.impl.KnockRequest +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable 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 kotlinx.collections.immutable.ImmutableList data class KnockRequestsBannerState( val isVisible: Boolean, - val knockRequests: ImmutableList, - val acceptAction: AsyncAction, + val knockRequests: ImmutableList, + val displayAcceptError: Boolean, val canAccept: Boolean, val eventSink: (KnockRequestsBannerEvents) -> Unit, ) { diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt index c0f4cc0961..460d1c7462 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt @@ -8,9 +8,8 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.knockrequests.impl.KnockRequest -import io.element.android.features.knockrequests.impl.aKnockRequest -import io.element.android.libraries.architecture.AsyncAction +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable +import io.element.android.features.knockrequests.impl.data.aKnockRequest import kotlinx.collections.immutable.toImmutableList class KnockRequestsBannerStateProvider : PreviewParameterProvider { @@ -44,10 +43,7 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider = listOf(aKnockRequest()), - acceptAction: AsyncAction = AsyncAction.Uninitialized, + knockRequests: List = listOf(aKnockRequest()), + displayAcceptError: Boolean = false, canAccept: Boolean = true, isVisible: Boolean = true, eventSink: (KnockRequestsBannerEvents) -> Unit = {} ) = KnockRequestsBannerState( knockRequests = knockRequests.toImmutableList(), - acceptAction = acceptAction, + displayAcceptError = displayAcceptError, canAccept = canAccept, isVisible = isVisible, eventSink = eventSink, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt index 9cf7144f5e..69dfa268a6 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt @@ -20,9 +20,12 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset @@ -37,9 +40,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.knockrequests.impl.KnockRequest import io.element.android.features.knockrequests.impl.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.AvatarSize 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.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList +import timber.log.Timber private const val MAX_AVATAR_COUNT = 3 @@ -61,22 +67,42 @@ fun KnockRequestsBannerView( onViewRequestsClick: () -> Unit, modifier: Modifier = Modifier, ) { - AnimatedVisibility( - visible = state.isVisible, - enter = expandVertically(), - exit = shrinkVertically(), - modifier = modifier, - ) { - Surface( - shape = MaterialTheme.shapes.small, - color = ElementTheme.colors.bgCanvasDefaultLevel1, - shadowElevation = 24.dp, - modifier = Modifier.padding(16.dp), + Box(modifier = modifier) { + AnimatedVisibility( + visible = state.isVisible, + enter = expandVertically(), + exit = shrinkVertically(), ) { - KnockRequestsBannerContent( - state = state, - onViewRequestsClick = onViewRequestsClick, - ) + Surface( + shape = MaterialTheme.shapes.small, + 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( - modifier - .fillMaxWidth() - .padding(all = 16.dp) + modifier + .fillMaxWidth() + .padding(all = 16.dp) ) { Row { KnockRequestAvatarView( @@ -122,13 +148,15 @@ private fun KnockRequestsBannerContent( ) } } + Spacer(modifier = Modifier.width(4.dp)) Icon( modifier = Modifier.clickable(onClick = ::onDismissClick), imageVector = CompoundIcons.Close(), contentDescription = stringResource(CommonStrings.action_close) ) } - if (state.reason != null) { + val reason = state.reason + if (!reason.isNullOrEmpty()) { Spacer(modifier = Modifier.height(16.dp)) Text( text = state.reason, @@ -169,7 +197,7 @@ private fun KnockRequestsBannerContent( @Composable private fun KnockRequestAvatarView( - knockRequests: ImmutableList, + knockRequests: ImmutableList, modifier: Modifier = Modifier, ) { Box(modifier) { @@ -183,7 +211,7 @@ private fun KnockRequestAvatarView( @Composable private fun KnockRequestAvatarListView( - knockRequests: ImmutableList, + knockRequests: ImmutableList, modifier: Modifier = Modifier, ) { val avatarSize = AvatarSize.KnockRequestBanner.dp @@ -198,27 +226,27 @@ private fun KnockRequestAvatarListView( smallReversedList.forEachIndexed { index, knockRequest -> Avatar( modifier = Modifier - .padding(start = avatarSize / 2 * (lastItemIndex - index)) - .graphicsLayer { - 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, - ) + .padding(start = avatarSize / 2 * (lastItemIndex - index)) + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen } - } - .size(size = avatarSize) - .padding(2.dp), + .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, + ) + } + } + .size(size = avatarSize) + .padding(2.dp), avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner), ) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt new file mode 100644 index 0000000000..5fbc2bb047 --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt @@ -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 +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt new file mode 100644 index 0000000000..6716908a56 --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt @@ -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 + } + +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt new file mode 100644 index 0000000000..56bcc34422 --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt @@ -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 = knockRequest.accept() + + suspend fun decline(reason: String?): Result = knockRequest.decline(reason) + + suspend fun declineAndBan(reason: String?): Result = knockRequest.declineAndBan(reason) + + suspend fun markAsSeen(): Result = knockRequest.markAsSeen() +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt new file mode 100644 index 0000000000..653930b16b --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -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>(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 { + 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 { + 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 { + 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 = 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) + ): Result { + 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(IllegalArgumentException("Knock request not found")) + + private fun MatrixRoom.wrappedKnockRequestsFlow() = knockRequestsFlow.map { knockRequests -> + knockRequests.map { KnockRequestWrapper(it) } + } +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt index 132c137ce2..23b1025ce2 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListEvents.kt @@ -7,12 +7,14 @@ 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 { - data class Accept(val knockRequest: KnockRequest) : KnockRequestsListEvents - data class Decline(val knockRequest: KnockRequest) : KnockRequestsListEvents - data class DeclineAndBan(val knockRequest: KnockRequest) : KnockRequestsListEvents + data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents + data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents + data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsListEvents data object AcceptAll : KnockRequestsListEvents - data object DismissCurrentAction : KnockRequestsListEvents + data object ResetCurrentAction : KnockRequestsListEvents + data object RetryCurrentAction : KnockRequestsListEvents + data object ConfirmCurrentAction : KnockRequestsListEvents } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index fcafc5428b..04bc48dd4b 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -11,70 +11,109 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf 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.AsyncData 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.ui.room.canBanAsState import io.element.android.libraries.matrix.ui.room.canInviteAsState import io.element.android.libraries.matrix.ui.room.canKickAsState -import kotlinx.collections.immutable.persistentListOf import javax.inject.Inject class KnockRequestsListPresenter @Inject constructor( private val room: MatrixRoom, + private val knockRequestsService: KnockRequestsService, ) : Presenter { @Composable override fun present(): KnockRequestsListState { - val currentAction = remember { mutableStateOf(KnockRequestsCurrentAction.None) } + val asyncAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var actionTarget by remember { mutableStateOf(KnockRequestsActionTarget.None) } + var targetActionConfirmed by remember { mutableStateOf(false) } + var retryCount by remember { mutableIntStateOf(0) } + val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canBan by room.canBanAsState(syncUpdateFlow.value) val canDecline by room.canKickAsState(syncUpdateFlow.value) val canAccept by room.canInviteAsState(syncUpdateFlow.value) + val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState() + fun handleEvents(event: KnockRequestsListEvents) { when (event) { KnockRequestsListEvents.AcceptAll -> { - currentAction.value = KnockRequestsCurrentAction.AcceptAll(AsyncAction.Uninitialized) + actionTarget = KnockRequestsActionTarget.AcceptAll } is KnockRequestsListEvents.Accept -> { - currentAction.value = KnockRequestsCurrentAction.Accept(event.knockRequest, AsyncAction.Uninitialized) + actionTarget = KnockRequestsActionTarget.Accept(event.knockRequest) } is KnockRequestsListEvents.Decline -> { - currentAction.value = KnockRequestsCurrentAction.Decline(event.knockRequest, AsyncAction.Uninitialized) + actionTarget = KnockRequestsActionTarget.Decline(event.knockRequest) } is KnockRequestsListEvents.DeclineAndBan -> { - currentAction.value = KnockRequestsCurrentAction.DeclineAndBan(event.knockRequest, AsyncAction.Uninitialized) + actionTarget = KnockRequestsActionTarget.DeclineAndBan(event.knockRequest) } - KnockRequestsListEvents.DismissCurrentAction -> { - currentAction.value = KnockRequestsCurrentAction.None + KnockRequestsListEvents.ResetCurrentAction -> { + actionTarget = KnockRequestsActionTarget.None + } + KnockRequestsListEvents.RetryCurrentAction -> { + retryCount++ + } + KnockRequestsListEvents.ConfirmCurrentAction -> { + targetActionConfirmed = true } } } - LaunchedEffect(currentAction) { - when (currentAction.value) { - is KnockRequestsCurrentAction.Accept -> { - // Accept the knock request + LaunchedEffect(actionTarget, targetActionConfirmed, retryCount) { + when (val action = actionTarget) { + is KnockRequestsActionTarget.Accept -> { + runUpdatingState(asyncAction) { + knockRequestsService.acceptKnockRequest(action.knockRequest) + } } - is KnockRequestsCurrentAction.Decline -> { - // Decline the knock request + is KnockRequestsActionTarget.Decline -> { + if (targetActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.declineKnockRequest(action.knockRequest) + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } } - is KnockRequestsCurrentAction.DeclineAndBan -> { - // Decline and ban the user + is KnockRequestsActionTarget.DeclineAndBan -> { + if (targetActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.declineAndBanKnockRequest(action.knockRequest) + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } } - is KnockRequestsCurrentAction.AcceptAll -> { - // Accept all knock requests + is KnockRequestsActionTarget.AcceptAll -> { + if (targetActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.acceptAllKnockRequests() + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } + } + KnockRequestsActionTarget.None -> { + targetActionConfirmed = false + asyncAction.value = AsyncAction.Uninitialized } - KnockRequestsCurrentAction.None -> Unit } } return KnockRequestsListState( - knockRequests = AsyncData.Success(persistentListOf()), - currentAction = currentAction.value, + knockRequests = knockRequests, + actionTarget = actionTarget, + asyncAction = asyncAction.value, canAccept = canAccept, canDecline = canDecline, canBan = canBan, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt index 3ba10e9302..763305eca2 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt @@ -8,27 +8,28 @@ package io.element.android.features.knockrequests.impl.list 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.AsyncData import kotlinx.collections.immutable.ImmutableList data class KnockRequestsListState( - val knockRequests: AsyncData>, - val currentAction: KnockRequestsCurrentAction, + val knockRequests: AsyncData>, + val actionTarget: KnockRequestsActionTarget, + val asyncAction: AsyncAction, val canAccept: Boolean, val canDecline: Boolean, val canBan: Boolean, 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 -sealed interface KnockRequestsCurrentAction { - data object None : KnockRequestsCurrentAction - data class Accept(val knockRequest: KnockRequest, val async: AsyncAction) : KnockRequestsCurrentAction - data class Decline(val knockRequest: KnockRequest, val async: AsyncAction) : KnockRequestsCurrentAction - data class DeclineAndBan(val knockRequest: KnockRequest, val async: AsyncAction) : KnockRequestsCurrentAction - data class AcceptAll(val async: AsyncAction) : KnockRequestsCurrentAction +sealed interface KnockRequestsActionTarget { + data object None : KnockRequestsActionTarget + data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget + data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget + data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget + data object AcceptAll : KnockRequestsActionTarget } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index 476bf556e1..e7207cced1 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -8,8 +8,8 @@ package io.element.android.features.knockrequests.impl.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.knockrequests.impl.KnockRequest -import io.element.android.features.knockrequests.impl.aKnockRequest +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable +import io.element.android.features.knockrequests.impl.data.aKnockRequest import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId @@ -64,7 +64,17 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider> = AsyncData.Success(persistentListOf()), - currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None, + knockRequests: AsyncData> = AsyncData.Success(persistentListOf()), + actionTarget: KnockRequestsActionTarget = KnockRequestsActionTarget.None, + asyncAction: AsyncAction = AsyncAction.Uninitialized, canAccept: Boolean = true, canDecline: Boolean = true, canBan: Boolean = true, eventSink: (KnockRequestsListEvents) -> Unit = {}, ) = KnockRequestsListState( knockRequests = knockRequests, - currentAction = currentAction, + actionTarget = actionTarget, + asyncAction = asyncAction, canAccept = canAccept, canDecline = canDecline, canBan = canBan, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index 41b5553438..58ac4984f4 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -46,23 +46,25 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.features.knockrequests.impl.KnockRequest import io.element.android.features.knockrequests.impl.R -import io.element.android.features.knockrequests.impl.getAvatarData -import io.element.android.features.knockrequests.impl.getBestName +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable +import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule 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.avatar.Avatar 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.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toDp 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.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.Icon 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.TextButton 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 kotlinx.collections.immutable.ImmutableList @@ -100,14 +103,18 @@ private fun KnockRequestsListContent( state: KnockRequestsListState, modifier: Modifier = Modifier, ) { - fun onAcceptClick(knockRequest: KnockRequest) { + fun onAcceptClick(knockRequest: KnockRequestPresentable) { state.eventSink(KnockRequestsListEvents.Accept(knockRequest)) } - fun onDeclineClick(knockRequest: KnockRequest) { + fun onDeclineClick(knockRequest: KnockRequestPresentable) { state.eventSink(KnockRequestsListEvents.Decline(knockRequest)) } + fun onBanClick(knockRequest: KnockRequestPresentable) { + state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequest)) + } + var bottomPaddingInPixels by remember { mutableIntStateOf(0) } Box(modifier.fillMaxSize()) { @@ -124,16 +131,30 @@ private fun KnockRequestsListContent( canBan = state.canBan, onAcceptClick = ::onAcceptClick, onDeclineClick = ::onDeclineClick, + onBanClick = ::onBanClick, contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()), ) } } + is AsyncData.Loading -> { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + color = ElementTheme.colors.iconPrimary, + ) + } else -> Unit } KnockRequestsActionsView( - actions = state.currentAction, + actionTarget = state.actionTarget, + asyncAction = state.asyncAction, + onConfirm = { + state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) + }, + onRetry = { + state.eventSink(KnockRequestsListEvents.RetryCurrentAction) + }, onDismiss = { - state.eventSink(KnockRequestsListEvents.DismissCurrentAction) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) }, ) if (state.canAcceptAll) { @@ -152,53 +173,45 @@ private fun KnockRequestsListContent( @Composable private fun KnockRequestsActionsView( - actions: KnockRequestsCurrentAction, + actionTarget: KnockRequestsActionTarget, + asyncAction: AsyncAction, + onConfirm: () -> Unit, onDismiss: () -> Unit, + onRetry: () -> Unit, modifier: Modifier = Modifier, ) { Box(modifier) { - when (actions) { - is KnockRequestsCurrentAction.AcceptAll -> { - AsyncActionView( - async = actions.async, - onSuccess = {}, - onErrorDismiss = onDismiss, + AsyncActionView( + async = asyncAction, + onSuccess = { onDismiss() }, + onErrorDismiss = onDismiss, + confirmationDialog = { + ConfirmationDialog( + title = "Confirmation", + content = "Are you sure?", + onSubmitClick = onConfirm, + onDismiss = onDismiss, ) - } - is KnockRequestsCurrentAction.Accept -> { - AsyncActionView( - async = actions.async, - onSuccess = {}, - onErrorDismiss = onDismiss, + }, + progressDialog = { + ProgressDialog( + text = "Loading", ) - } - is KnockRequestsCurrentAction.Decline -> { - AsyncActionView( - async = actions.async, - onSuccess = {}, - onErrorDismiss = onDismiss, - ) - } - is KnockRequestsCurrentAction.DeclineAndBan -> { - AsyncActionView( - async = actions.async, - onSuccess = {}, - onErrorDismiss = onDismiss, - ) - } - KnockRequestsCurrentAction.None -> Unit - } + }, + onRetry = onRetry, + ) } } @Composable private fun KnockRequestsList( - knockRequests: ImmutableList, + knockRequests: ImmutableList, canAccept: Boolean, canDecline: Boolean, canBan: Boolean, - onAcceptClick: (KnockRequest) -> Unit, - onDeclineClick: (KnockRequest) -> Unit, + onAcceptClick: (KnockRequestPresentable) -> Unit, + onDeclineClick: (KnockRequestPresentable) -> Unit, + onBanClick: (KnockRequestPresentable) -> Unit, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { @@ -214,6 +227,7 @@ private fun KnockRequestsList( canDecline = canDecline, canAccept = canAccept, onDeclineClick = onDeclineClick, + onBanClick = onBanClick, ) if (index != knockRequests.size - 1) { HorizontalDivider() @@ -224,12 +238,13 @@ private fun KnockRequestsList( @Composable private fun KnockRequestItem( - knockRequest: KnockRequest, + knockRequest: KnockRequestPresentable, canAccept: Boolean, canDecline: Boolean, canBan: Boolean, - onAcceptClick: (KnockRequest) -> Unit, - onDeclineClick: (KnockRequest) -> Unit, + onAcceptClick: (KnockRequestPresentable) -> Unit, + onDeclineClick: (KnockRequestPresentable) -> Unit, + onBanClick: (KnockRequestPresentable) -> Unit, modifier: Modifier = Modifier, ) { Row( @@ -252,10 +267,11 @@ private fun KnockRequestItem( color = MaterialTheme.colorScheme.primary, style = ElementTheme.typography.fontBodyLgMedium, ) - if (!knockRequest.formattedDate.isNullOrEmpty()) { + val formattedDate = knockRequest.formattedDate + if (!formattedDate.isNullOrEmpty()) { Spacer(modifier = Modifier.width(8.dp)) Text( - text = knockRequest.formattedDate, + text = formattedDate, color = MaterialTheme.colorScheme.secondary, style = ElementTheme.typography.fontBodySmRegular, ) @@ -272,7 +288,8 @@ private fun KnockRequestItem( ) } // Reason - if (!knockRequest.reason.isNullOrBlank()) { + val reason = knockRequest.reason + if (!reason.isNullOrBlank()) { Spacer(modifier = Modifier.height(12.dp)) var isExpanded 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 } ) { Text( - text = knockRequest.reason, + text = reason, style = ElementTheme.typography.fontBodyMdRegular, maxLines = if (isExpanded) Int.MAX_VALUE else 3, onTextLayout = { result -> @@ -336,7 +353,7 @@ private fun KnockRequestItem( TextButton( text = stringResource(R.string.screen_knock_requests_list_decline_and_ban_action_title), onClick = { - onAcceptClick(knockRequest) + onBanClick(knockRequest) }, destructive = true, size = ButtonSize.Small, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 82ea24e7ff..d028c609ed 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -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.widget.MatrixWidgetDriver import io.element.android.libraries.matrix.api.widget.MatrixWidgetSettings +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.io.Closeable @@ -53,6 +54,8 @@ interface MatrixRoom : Closeable { val activeMemberCount: Long val joinedMemberCount: Long + val roomCoroutineScope: CoroutineScope + val roomInfoFlow: Flow val roomTypingMembersFlow: Flow> val identityStateChangesFlow: Flow> diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt index f5b25b5f2c..e7a0488245 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/knock/KnockRequest.kt @@ -7,14 +7,17 @@ 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 interface KnockRequest { + val eventId: EventId val userId: UserId val displayName: String? val avatarUrl: String? val reason: String? val timestamp: Long? + val isSeen: Boolean suspend fun accept(): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 44f588bd83..588927f434 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -79,7 +79,7 @@ import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener 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.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem @@ -162,7 +162,7 @@ class RustMatrixRoom( } override val knockRequestsFlow: Flow> = mxCallbackFlow { - innerRoom.subscribeToJoinRequests(object : RequestsToJoinListener { + innerRoom.subscribeToJoinRequests(object : JoinRequestsListener { override fun call(joinRequests: List) { val knockRequests = joinRequests.map { RustKnockRequest(it) } channel.trySend(knockRequests) @@ -176,7 +176,7 @@ class RustMatrixRoom( // ...except getMember methods as it could quickly fill the roomDispatcher... 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 roomMemberListFetcher = RoomMemberListFetcher(innerRoom, roomMembersDispatcher) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt index a3d133d599..8f7b075511 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt @@ -7,6 +7,7 @@ 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.room.knock.KnockRequest import org.matrix.rustcomponents.sdk.JoinRequest @@ -14,11 +15,13 @@ import org.matrix.rustcomponents.sdk.JoinRequest class RustKnockRequest( private val inner: JoinRequest, ) : KnockRequest { + override val eventId: EventId = EventId(inner.eventId) override val userId: UserId = UserId(inner.userId) override val displayName: String? = inner.displayName override val avatarUrl: String? = inner.avatarUrl override val reason: String? = inner.reason override val timestamp: Long? = inner.timestamp?.toLong() + override val isSeen: Boolean = inner.isSeen override suspend fun accept(): Result = runCatching { inner.actions.accept() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index cfb73e267e..18b804455d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -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.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.test.TestScope import java.io.File class FakeMatrixRoom( @@ -73,6 +75,7 @@ class FakeMatrixRoom( override val activeMemberCount: Long = 234L, val notificationSettingsService: NotificationSettingsService = FakeNotificationSettingsService(), override val liveTimeline: Timeline = FakeTimeline(), + override val roomCoroutineScope: CoroutineScope = TestScope(), private var roomPermalinkResult: () -> Result = { lambdaError() }, private var eventPermalinkResult: (EventId) -> Result = { lambdaError() }, private val sendCallNotificationIfNeededResult: () -> Result = { lambdaError() }, From 33c02c1f2c5fe9bc0934fb74b0bc93227285dd9d Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Dec 2024 11:07:38 +0100 Subject: [PATCH 03/18] knock requests : sync localazy --- .../impl/src/main/res/values/localazy.xml | 11 +++++++++++ libraries/ui-strings/src/main/res/values/localazy.xml | 1 + 2 files changed, 12 insertions(+) diff --git a/features/knockrequests/impl/src/main/res/values/localazy.xml b/features/knockrequests/impl/src/main/res/values/localazy.xml index d2e9dc5d14..454bb8e193 100644 --- a/features/knockrequests/impl/src/main/res/values/localazy.xml +++ b/features/knockrequests/impl/src/main/res/values/localazy.xml @@ -4,15 +4,26 @@ "Are you sure you want to accept all requests to join?" "Accept all requests" "Accept all" + "We couldn’t accept all requests. Would you like to try again?" + "Failed to accept all requests" + "Accepting all requests to join" + "We couldn’t accept this request. Would you like to try again?" + "Failed to accept request" + "Accepting request to join" "Yes, decline and ban" "Are you sure you want to decline and ban %1$s? This user won’t be able to request access to join this room again." "Decline and ban from accessing" + "Declining and banning access" "Yes, decline" "Are you sure you want to decline %1$s request to join this room?" "Decline access" "Decline and ban" + "We couldn’t decline this request. Would you like to try again?" + "Failed to decline request" + "Declining request to join" "When somebody will ask to join the room, you’ll be able to see their request here." "No pending request to join" + "Loading requests to join…" "Requests to join" "%1$s +%2$d other want to join this room" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index b571a23230..ab38b9b148 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -127,6 +127,7 @@ "View in timeline" "View source" "Yes" + "Yes, try again" "About" "Acceptable use policy" "Adding caption" From 759cd0f0e0518e846609ece5c22debff6911b946 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Dec 2024 12:09:14 +0100 Subject: [PATCH 04/18] knock requests : manage remaining ui states --- .../impl/list/KnockRequestsListPresenter.kt | 7 +- .../impl/list/KnockRequestsListView.kt | 87 ++++++++++++++++--- 2 files changed, 78 insertions(+), 16 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index 04bc48dd4b..3a0d4ba7ae 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -58,7 +58,9 @@ class KnockRequestsListPresenter @Inject constructor( actionTarget = KnockRequestsActionTarget.DeclineAndBan(event.knockRequest) } KnockRequestsListEvents.ResetCurrentAction -> { + asyncAction.value = AsyncAction.Uninitialized actionTarget = KnockRequestsActionTarget.None + targetActionConfirmed = false } KnockRequestsListEvents.RetryCurrentAction -> { retryCount++ @@ -103,10 +105,7 @@ class KnockRequestsListPresenter @Inject constructor( asyncAction.value = AsyncAction.ConfirmingNoParams } } - KnockRequestsActionTarget.None -> { - targetActionConfirmed = false - asyncAction.value = AsyncAction.Uninitialized - } + KnockRequestsActionTarget.None -> Unit } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index 58ac4984f4..e928b3a726 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -11,6 +11,7 @@ import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -72,7 +73,6 @@ 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.TextButton 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 kotlinx.collections.immutable.ImmutableList @@ -137,10 +137,18 @@ private fun KnockRequestsListContent( } } is AsyncData.Loading -> { - CircularProgressIndicator( - modifier = Modifier.align(Alignment.Center), - color = ElementTheme.colors.iconPrimary, - ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = spacedBy(16.dp), + modifier = Modifier.align(Alignment.Center) + ) { + CircularProgressIndicator(color = ElementTheme.colors.iconPrimary) + Text( + text = stringResource(R.string.screen_knock_requests_list_initial_loading_title), + style = ElementTheme.typography.fontBodyLgRegular, + color = ElementTheme.colors.textPrimary, + ) + } } else -> Unit } @@ -186,23 +194,78 @@ private fun KnockRequestsActionsView( onSuccess = { onDismiss() }, onErrorDismiss = onDismiss, confirmationDialog = { - ConfirmationDialog( - title = "Confirmation", - content = "Are you sure?", - onSubmitClick = onConfirm, + KnockRequestActionConfirmation( + actionTarget = actionTarget, + onSubmit = onConfirm, onDismiss = onDismiss, ) }, progressDialog = { - ProgressDialog( - text = "Loading", - ) + KnockRequestActionProgress(target = actionTarget) + }, + errorMessage = { + when (actionTarget) { + is KnockRequestsActionTarget.Accept -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + else -> "" + } }, onRetry = onRetry, ) } } +@Composable +private fun KnockRequestActionConfirmation( + actionTarget: KnockRequestsActionTarget, + onSubmit: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val (title, content) = when (actionTarget) { + KnockRequestsActionTarget.AcceptAll -> Pair( + stringResource(R.string.screen_knock_requests_list_accept_all_alert_title), + stringResource(R.string.screen_knock_requests_list_accept_all_alert_description), + ) + is KnockRequestsActionTarget.Decline -> Pair( + stringResource(R.string.screen_knock_requests_list_decline_alert_title), + stringResource(R.string.screen_knock_requests_list_decline_alert_description, actionTarget.knockRequest.getBestName()), + ) + is KnockRequestsActionTarget.DeclineAndBan -> Pair( + stringResource(R.string.screen_knock_requests_list_ban_alert_title), + stringResource(R.string.screen_knock_requests_list_ban_alert_description, actionTarget.knockRequest.getBestName()), + ) + else -> return + } + ConfirmationDialog( + title = title, + content = content, + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) +} + +@Composable +private fun KnockRequestActionProgress( + target: KnockRequestsActionTarget, + modifier: Modifier = Modifier, +) { + val progressText = when (target) { + is KnockRequestsActionTarget.Accept -> stringResource(R.string.screen_knock_requests_list_accept_loading_title) + is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_decline_loading_title) + is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_ban_loading_title) + KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_loading_title) + else -> return + } + ProgressDialog( + text = progressText, + modifier = modifier, + ) +} + @Composable private fun KnockRequestsList( knockRequests: ImmutableList, From 5715f6054a55afcd95a784729181ae8628592af2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Dec 2024 13:10:23 +0100 Subject: [PATCH 05/18] knock request : expose JoinRule from sdk --- .../matrix/api/room/MatrixRoomInfo.kt | 2 ++ .../matrix/api/room/join/AllowRule.kt | 15 ++++++++++++ .../matrix/api/room/join/JoinRule.kt | 18 +++++++++++++++ .../matrix/impl/room/MatrixRoomInfoMapper.kt | 2 ++ .../matrix/impl/room/join/AllowRule.kt | 19 +++++++++++++++ .../matrix/impl/room/join/JoinRule.kt | 23 +++++++++++++++++++ .../impl/room/MatrixRoomInfoMapperTest.kt | 6 +++++ .../matrix/test/room/RoomInfoFixture.kt | 3 +++ .../matrix/test/room/RoomSummaryFixture.kt | 3 +++ 9 files changed, 91 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt index 6105a59c38..825ff85e96 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoomInfo.kt @@ -12,6 +12,7 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomAlias import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.user.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap @@ -27,6 +28,7 @@ data class MatrixRoomInfo( val avatarUrl: String?, val isDirect: Boolean, val isPublic: Boolean, + val joinRule: JoinRule?, val isSpace: Boolean, val isTombstoned: Boolean, val isFavorite: Boolean, diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt new file mode 100644 index 0000000000..2ba4893ec8 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/AllowRule.kt @@ -0,0 +1,15 @@ +/* + * 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.libraries.matrix.api.room.join + +import io.element.android.libraries.matrix.api.core.RoomId + +sealed interface AllowRule { + data class RoomMembership(val roomId: RoomId) : AllowRule + data class Custom(val json: String) : AllowRule +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt new file mode 100644 index 0000000000..33c2ccf092 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt @@ -0,0 +1,18 @@ +/* + * 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.libraries.matrix.api.room.join + +sealed interface JoinRule { + data object Public : JoinRule + data object Private: JoinRule + data object Knock: JoinRule + data object Invite: JoinRule + data class Restricted(val rules: List): JoinRule + data class KnockRestricted(val rules: List): JoinRule + data class Custom(val value: String): JoinRule +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt index 607c316b25..2d96a8c970 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapper.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomNotificationMode import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.impl.room.join.map import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.toImmutableList @@ -36,6 +37,7 @@ class MatrixRoomInfoMapper { avatarUrl = it.avatarUrl, isDirect = it.isDirect, isPublic = it.isPublic, + joinRule = it.joinRule?.map(), isSpace = it.isSpace, isTombstoned = it.isTombstoned, isFavorite = it.isFavourite, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt new file mode 100644 index 0000000000..86f37c5f20 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/AllowRule.kt @@ -0,0 +1,19 @@ +/* + * 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.libraries.matrix.impl.room.join + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.join.AllowRule +import org.matrix.rustcomponents.sdk.AllowRule as RustAllowRule + +fun RustAllowRule.map(): AllowRule { + return when (this) { + is RustAllowRule.RoomMembership -> AllowRule.RoomMembership(RoomId(roomId)) + is RustAllowRule.Custom -> AllowRule.Custom(json) + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt new file mode 100644 index 0000000000..f5c65c7283 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/join/JoinRule.kt @@ -0,0 +1,23 @@ +/* + * 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.libraries.matrix.impl.room.join + +import io.element.android.libraries.matrix.api.room.join.JoinRule +import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule + +fun RustJoinRule.map(): JoinRule { + return when (this) { + RustJoinRule.Public -> JoinRule.Public + RustJoinRule.Private -> JoinRule.Private + RustJoinRule.Knock -> JoinRule.Knock + RustJoinRule.Invite -> JoinRule.Invite + is RustJoinRule.Restricted -> JoinRule.Restricted(rules.map { it.map() }) + is RustJoinRule.Custom -> JoinRule.Custom(repr) + is RustJoinRule.KnockRestricted -> JoinRule.KnockRestricted(rules.map { it.map() }) + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt index d2fa714880..9f91ed16de 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -14,6 +14,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomHero import io.element.android.libraries.matrix.impl.fixtures.factories.aRustRoomInfo @@ -30,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList import org.junit.Test +import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.Membership import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode @@ -47,6 +49,7 @@ class MatrixRoomInfoMapperTest { isDirect = true, isPublic = false, isSpace = false, + joinRule = RustJoinRule.Invite, isTombstoned = false, isFavourite = false, canonicalAlias = A_ROOM_ALIAS.value, @@ -83,6 +86,7 @@ class MatrixRoomInfoMapperTest { isSpace = false, isTombstoned = false, isFavorite = false, + joinRule = JoinRule.Invite, canonicalAlias = A_ROOM_ALIAS, alternativeAliases = listOf(A_ROOM_ALIAS).toImmutableList(), currentUserMembership = CurrentUserMembership.JOINED, @@ -125,6 +129,7 @@ class MatrixRoomInfoMapperTest { avatarUrl = null, isDirect = false, isPublic = true, + joinRule = null, isSpace = false, isTombstoned = false, isFavourite = true, @@ -159,6 +164,7 @@ class MatrixRoomInfoMapperTest { avatarUrl = null, isDirect = false, isPublic = true, + joinRule = null, isSpace = false, isTombstoned = false, isFavorite = true, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt index 5aae1a3258..c05c24e7de 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomInfoFixture.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -33,6 +34,7 @@ fun aRoomInfo( avatarUrl: String? = AN_AVATAR_URL, isDirect: Boolean = false, isPublic: Boolean = true, + joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, isTombstoned: Boolean = false, isFavorite: Boolean = false, @@ -64,6 +66,7 @@ fun aRoomInfo( avatarUrl = avatarUrl, isDirect = isDirect, isPublic = isPublic, + joinRule = joinRule, isSpace = isSpace, isTombstoned = isTombstoned, isFavorite = isFavorite, diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 39e13cb619..cd612f5ec1 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -15,6 +15,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership import io.element.android.libraries.matrix.api.room.MatrixRoomInfo import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.RoomNotificationMode +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.roomlist.RoomSummary import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem @@ -46,6 +47,7 @@ fun aRoomSummary( avatarUrl: String? = null, isDirect: Boolean = false, isPublic: Boolean = true, + joinRule: JoinRule? = JoinRule.Public, isSpace: Boolean = false, isTombstoned: Boolean = false, isFavorite: Boolean = false, @@ -79,6 +81,7 @@ fun aRoomSummary( avatarUrl = avatarUrl, isDirect = isDirect, isPublic = isPublic, + joinRule = joinRule, isSpace = isSpace, isTombstoned = isTombstoned, isFavorite = isFavorite, From 00169c7be217c579cb9df3a523453755920afbcb Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 16 Dec 2024 20:32:17 +0100 Subject: [PATCH 06/18] knock requests : makes knock can be handled and is enabled --- features/knockrequests/impl/build.gradle.kts | 1 + .../impl/banner/KnockRequestsBannerPresenter.kt | 8 +++++++- .../roomdetails/impl/RoomDetailsPresenter.kt | 16 ++++++++++------ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts index 83f7132320..b664a0a6b4 100644 --- a/features/knockrequests/impl/build.gradle.kts +++ b/features/knockrequests/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) implementation(projects.libraries.designsystem) + implementation(projects.libraries.featureflag.api) testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt index c95fca6a5f..a1ad661b0c 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt @@ -19,7 +19,10 @@ import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.extensions.firstIfSingle +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState import io.element.android.libraries.matrix.ui.room.canInviteAsState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope @@ -33,6 +36,7 @@ class KnockRequestsBannerPresenter @Inject constructor( private val room: MatrixRoom, private val knockRequestsService: KnockRequestsService, private val appCoroutineScope: CoroutineScope, + private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): KnockRequestsBannerState { @@ -46,11 +50,13 @@ class KnockRequestsBannerPresenter @Inject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canAccept by room.canInviteAsState(syncUpdateFlow.value) + val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) val showAcceptError = remember { mutableStateOf(false) } + val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false) val shouldShowBanner by remember { derivedStateOf { - knockRequests.isNotEmpty() + isKnockRequestsEnabled && canHandleKnockRequests && knockRequests.isNotEmpty() } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index d55f4e4f7d..0fc907a867 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -36,6 +36,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.isDm +import io.element.android.libraries.matrix.api.room.join.JoinRule import io.element.android.libraries.matrix.api.room.powerlevels.canInvite import io.element.android.libraries.matrix.api.room.powerlevels.canSendState import io.element.android.libraries.matrix.api.room.roomNotificationSettings @@ -47,7 +48,9 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -77,7 +80,7 @@ class RoomDetailsPresenter @Inject constructor( val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.displayName).trim() } } val roomTopic by remember { derivedStateOf { roomInfo?.topic ?: room.topic } } val isFavorite by remember { derivedStateOf { roomInfo?.isFavorite.orFalse() } } - val isPublic by remember { derivedStateOf { roomInfo?.isPublic.orFalse() } } + val joinRule by remember { derivedStateOf { roomInfo?.joinRule } } val canShowPinnedMessages = isPinnedMessagesFeatureEnabled() var canShowMediaGallery by remember { mutableStateOf(false) } @@ -106,11 +109,9 @@ class RoomDetailsPresenter @Inject constructor( val roomType by getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() - val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic - when { !topic.isNullOrBlank() -> RoomTopicState.ExistingTopic(topic) canEditTopic && roomType is RoomDetailsType.Room -> RoomTopicState.CanAddTopic @@ -118,10 +119,13 @@ class RoomDetailsPresenter @Inject constructor( } } + val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false) - val knockRequestsCount by remember { mutableStateOf(null) } + val knockRequestsCount by produceState(null) { + room.knockRequestsFlow.collect { value = it.size } + } val canShowKnockRequests by remember { - derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests } + derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests && joinRule == JoinRule.Knock } } val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState() @@ -164,7 +168,7 @@ class RoomDetailsPresenter @Inject constructor( roomNotificationSettings = roomNotificationSettingsState.roomNotificationSettings(), isFavorite = isFavorite, displayRolesAndPermissionsSettings = !room.isDm && isUserAdmin, - isPublic = isPublic, + isPublic = joinRule == JoinRule.Public, heroes = roomInfo?.heroes.orEmpty().toPersistentList(), canShowPinnedMessages = canShowPinnedMessages, canShowMediaGallery = canShowMediaGallery, From 03925109da9cb9ba696ad58c7f2a32860a483524 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Dec 2024 15:35:44 +0100 Subject: [PATCH 07/18] knock request : simplify executing action --- .../impl/list/KnockRequestsListPresenter.kt | 95 ++++++++++--------- 1 file changed, 52 insertions(+), 43 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index 3a0d4ba7ae..d6e2adbebd 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -9,11 +9,12 @@ package io.element.android.features.knockrequests.impl.list import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.AsyncAction @@ -23,6 +24,8 @@ 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.canInviteAsState import io.element.android.libraries.matrix.ui.room.canKickAsState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch import javax.inject.Inject class KnockRequestsListPresenter @Inject constructor( @@ -33,8 +36,6 @@ class KnockRequestsListPresenter @Inject constructor( override fun present(): KnockRequestsListState { val asyncAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } var actionTarget by remember { mutableStateOf(KnockRequestsActionTarget.None) } - var targetActionConfirmed by remember { mutableStateOf(false) } - var retryCount by remember { mutableIntStateOf(0) } val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val canBan by room.canBanAsState(syncUpdateFlow.value) @@ -43,6 +44,8 @@ class KnockRequestsListPresenter @Inject constructor( val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState() + val coroutineScope = rememberCoroutineScope() + fun handleEvents(event: KnockRequestsListEvents) { when (event) { KnockRequestsListEvents.AcceptAll -> { @@ -60,53 +63,17 @@ class KnockRequestsListPresenter @Inject constructor( KnockRequestsListEvents.ResetCurrentAction -> { asyncAction.value = AsyncAction.Uninitialized actionTarget = KnockRequestsActionTarget.None - targetActionConfirmed = false } KnockRequestsListEvents.RetryCurrentAction -> { - retryCount++ + coroutineScope.executeAction(actionTarget, asyncAction, isActionConfirmed = true) } KnockRequestsListEvents.ConfirmCurrentAction -> { - targetActionConfirmed = true + coroutineScope.executeAction(actionTarget, asyncAction, isActionConfirmed = true) } } } - - LaunchedEffect(actionTarget, targetActionConfirmed, retryCount) { - when (val action = actionTarget) { - is KnockRequestsActionTarget.Accept -> { - runUpdatingState(asyncAction) { - knockRequestsService.acceptKnockRequest(action.knockRequest) - } - } - is KnockRequestsActionTarget.Decline -> { - if (targetActionConfirmed) { - runUpdatingState(asyncAction) { - knockRequestsService.declineKnockRequest(action.knockRequest) - } - } else { - asyncAction.value = AsyncAction.ConfirmingNoParams - } - } - is KnockRequestsActionTarget.DeclineAndBan -> { - if (targetActionConfirmed) { - runUpdatingState(asyncAction) { - knockRequestsService.declineAndBanKnockRequest(action.knockRequest) - } - } else { - asyncAction.value = AsyncAction.ConfirmingNoParams - } - } - is KnockRequestsActionTarget.AcceptAll -> { - if (targetActionConfirmed) { - runUpdatingState(asyncAction) { - knockRequestsService.acceptAllKnockRequests() - } - } else { - asyncAction.value = AsyncAction.ConfirmingNoParams - } - } - KnockRequestsActionTarget.None -> Unit - } + LaunchedEffect(actionTarget) { + executeAction(actionTarget, asyncAction, isActionConfirmed = false) } return KnockRequestsListState( @@ -119,4 +86,46 @@ class KnockRequestsListPresenter @Inject constructor( eventSink = ::handleEvents ) } + + private fun CoroutineScope.executeAction( + actionTarget: KnockRequestsActionTarget, + asyncAction: MutableState>, + isActionConfirmed: Boolean, + ) = launch { + when (actionTarget) { + is KnockRequestsActionTarget.Accept -> { + runUpdatingState(asyncAction) { + knockRequestsService.acceptKnockRequest(actionTarget.knockRequest) + } + } + is KnockRequestsActionTarget.Decline -> { + if (isActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.declineKnockRequest(actionTarget.knockRequest) + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } + } + is KnockRequestsActionTarget.DeclineAndBan -> { + if (isActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.declineAndBanKnockRequest(actionTarget.knockRequest) + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } + } + is KnockRequestsActionTarget.AcceptAll -> { + if (isActionConfirmed) { + runUpdatingState(asyncAction) { + knockRequestsService.acceptAllKnockRequests() + } + } else { + asyncAction.value = AsyncAction.ConfirmingNoParams + } + } + KnockRequestsActionTarget.None -> Unit + } + } } From 0b5dc40d402ac2f287c3e483971d4181531d6b1a Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Dec 2024 15:36:08 +0100 Subject: [PATCH 08/18] knock requests : make sure to use the correct confirmation submit text --- .../impl/list/KnockRequestsListView.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index e928b3a726..cee983050d 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -224,24 +224,29 @@ private fun KnockRequestActionConfirmation( onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val (title, content) = when (actionTarget) { - KnockRequestsActionTarget.AcceptAll -> Pair( + val (title, content, submitText) = when (actionTarget) { + KnockRequestsActionTarget.AcceptAll -> Triple( stringResource(R.string.screen_knock_requests_list_accept_all_alert_title), stringResource(R.string.screen_knock_requests_list_accept_all_alert_description), + stringResource(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title), + ) - is KnockRequestsActionTarget.Decline -> Pair( + is KnockRequestsActionTarget.Decline -> Triple( stringResource(R.string.screen_knock_requests_list_decline_alert_title), stringResource(R.string.screen_knock_requests_list_decline_alert_description, actionTarget.knockRequest.getBestName()), + stringResource(R.string.screen_knock_requests_list_decline_alert_confirm_button_title), ) - is KnockRequestsActionTarget.DeclineAndBan -> Pair( + is KnockRequestsActionTarget.DeclineAndBan -> Triple( stringResource(R.string.screen_knock_requests_list_ban_alert_title), stringResource(R.string.screen_knock_requests_list_ban_alert_description, actionTarget.knockRequest.getBestName()), + stringResource(R.string.screen_knock_requests_list_ban_alert_confirm_button_title), ) else -> return } ConfirmationDialog( title = title, content = content, + submitText = submitText, onSubmitClick = onSubmit, onDismiss = onDismiss, modifier = modifier, From e1f6c07ecd48c4aabd6c7a748fce78530c67094d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 17 Dec 2024 15:36:30 +0100 Subject: [PATCH 09/18] knock requests : add tests to the feature --- features/knockrequests/impl/build.gradle.kts | 10 + .../impl/data/KnockRequestsModule.kt | 25 ++ .../impl/data/KnockRequestsService.kt | 19 +- .../KnockRequestsBannerPresenterTest.kt | 253 ++++++++++++++ .../banner/KnockRequestsBannerViewTest.kt | 102 ++++++ .../list/KnockRequestsListPresenterTest.kt | 310 ++++++++++++++++++ .../impl/list/KnockRequestsListViewTest.kt | 163 +++++++++ .../test/room/knock/FakeKnockRequest.kt | 21 +- 8 files changed, 886 insertions(+), 17 deletions(-) create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt create mode 100644 features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt create mode 100644 features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt create mode 100644 features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt create mode 100644 features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt diff --git a/features/knockrequests/impl/build.gradle.kts b/features/knockrequests/impl/build.gradle.kts index b664a0a6b4..2664528d74 100644 --- a/features/knockrequests/impl/build.gradle.kts +++ b/features/knockrequests/impl/build.gradle.kts @@ -14,6 +14,11 @@ plugins { android { namespace = "io.element.android.features.knockrequests.impl" + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } setupAnvil() @@ -31,7 +36,12 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) testImplementation(libs.molecule.runtime) + testImplementation(libs.test.robolectric) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) + testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(projects.libraries.featureflag.test) + testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt new file mode 100644 index 0000000000..83f0a08c5b --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt @@ -0,0 +1,25 @@ +/* + * 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 com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.room.MatrixRoom + +@Module +@ContributesTo(RoomScope::class) +object KnockRequestsModule { + @Provides + @SingleIn(RoomScope::class) + fun knockRequestsService(room: MatrixRoom): KnockRequestsService { + return KnockRequestsService(room.knockRequestsFlow, room.roomCoroutineScope) + } +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index 653930b16b..5a373f3f36 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -8,13 +8,13 @@ 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 io.element.android.libraries.matrix.api.room.knock.KnockRequest import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine @@ -22,23 +22,24 @@ 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) { +class KnockRequestsService( + knockRequestsFlow: Flow>, + coroutineScope: CoroutineScope, +) { // 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>(emptySet()) val knockRequestsFlow = combine( - room.wrappedKnockRequestsFlow(), + knockRequestsFlow.wrapped(), handledKnockRequestIds, ) { knockRequests, handledKnockIds -> val presentableKnockRequests = knockRequests .filter { it.eventId !in handledKnockIds } .toImmutableList() AsyncData.Success(presentableKnockRequests) - }.stateIn(room.roomCoroutineScope, SharingStarted.Lazily, AsyncData.Loading()) + }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading()) private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty() @@ -129,7 +130,7 @@ class KnockRequestsService @Inject constructor(room: MatrixRoom) { private fun knockRequestNotFoundResult() = Result.failure(IllegalArgumentException("Knock request not found")) - private fun MatrixRoom.wrappedKnockRequestsFlow() = knockRequestsFlow.map { knockRequests -> + private fun Flow>.wrapped() = map { knockRequests -> knockRequests.map { KnockRequestWrapper(it) } } } diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt new file mode 100644 index 0000000000..189f392f95 --- /dev/null +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -0,0 +1,253 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.knockrequests.impl.banner + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.knockrequests.impl.data.KnockRequestsService +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService +import io.element.android.libraries.matrix.api.room.knock.KnockRequest +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.A_USER_ID_2 +import io.element.android.libraries.matrix.test.A_USER_ID_3 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest { + + @Test + fun `present - when feature is disabled then the banner should be hidden`() = runTest { + val knockRequests = flowOf(listOf(FakeKnockRequest())) + val presenter = createKnockRequestsBannerPresenter(isFeatureEnabled = false, knockRequestsFlow = knockRequests) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + } + } + } + + @Test + fun `present - when empty knock request list then the banner should be hidden`() = runTest { + val knockRequests = flowOf(emptyList()) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(1) + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + } + } + } + + @Test + fun `present - when no permission to manage knock requests then the banner should be hidden`() = runTest { + val presenter = createKnockRequestsBannerPresenter(canAcceptKnockRequests = false) + presenter.test { + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + } + } + } + + @Test + fun `present - when everything is setup to manage knocks with data, then the banner should be visible `() = runTest { + val knockRequests = flowOf( + listOf( + FakeKnockRequest( + reason = "A reason", + ) + ) + ) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.isVisible).isTrue() + assertThat(state.knockRequests).hasSize(1) + assertThat(state.canAccept).isTrue() + assertThat(state.reason).isEqualTo("A reason") + } + } + } + + @Test + fun `present - when multiple knock requests, the banner should not have reason nor subtitle`() = runTest { + val knockRequests = flowOf( + listOf( + FakeKnockRequest( + displayName = "Alice", + ), + FakeKnockRequest( + displayName = "Bob", + ), + FakeKnockRequest( + displayName = "Charlie", + ), + ) + ) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.isVisible).isTrue() + assertThat(state.knockRequests).hasSize(3) + assertThat(state.reason).isNull() + assertThat(state.subtitle).isNull() + } + } + } + + @Test + fun `present - when there are some seen knock requests, then the banner should filtered them`() = runTest { + val knockRequests = flowOf( + listOf( + FakeKnockRequest( + displayName = "Alice", + isSeen = true, + userId = A_USER_ID + ), + FakeKnockRequest( + displayName = "Bob", + isSeen = true, + userId = A_USER_ID_2 + ), + FakeKnockRequest( + isSeen = false, + displayName = "Charlie", + reason = "A reason", + userId = A_USER_ID_3 + ), + ) + ) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.isVisible).isTrue() + // Only Charlie should be displayed + assertThat(state.knockRequests).hasSize(1) + assertThat(state.reason).isEqualTo("A reason") + assertThat(state.subtitle).isEqualTo(A_USER_ID_3.value) + } + } + } + + @Test + fun `present - given AcceptSingleRequest event with failure, then the banner should hide and reappear and error should appear and disappear`() = runTest { + val acceptLambda = lambdaRecorder> { Result.failure(Exception()) } + val knockRequest = FakeKnockRequest( + displayName = "Alice", + reason = "A reason", + acceptLambda = acceptLambda + ) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest) + } + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + assertThat(state.displayAcceptError).isFalse() + } + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + assertThat(state.displayAcceptError).isTrue() + } + awaitItem().also { state -> + assertThat(state.isVisible).isTrue() + assertThat(state.displayAcceptError).isTrue() + } + awaitItem().also { state -> + assertThat(state.isVisible).isTrue() + assertThat(state.displayAcceptError).isFalse() + } + assert(acceptLambda).isCalledOnce() + } + } + + @Test + fun `present - given an AcceptSingleRequest event with success, then banner should be dismissed`() = runTest { + val acceptLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequest = FakeKnockRequest( + displayName = "Alice", + reason = "A reason", + acceptLambda = acceptLambda + ) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.knockRequests).hasSize(1) + state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest) + } + awaitItem().also { state -> + assertThat(state.isVisible).isFalse() + } + advanceUntilIdle() + assert(acceptLambda).isCalledOnce() + } + } + + @Test + fun `present - given a Dismiss event, then knock requests should be marked as seen`() = runTest { + val markAsSeenLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequests = flowOf( + listOf( + FakeKnockRequest(markAsSeenLambda = markAsSeenLambda), + FakeKnockRequest(markAsSeenLambda = markAsSeenLambda), + FakeKnockRequest(markAsSeenLambda = markAsSeenLambda), + ) + ) + val presenter = createKnockRequestsBannerPresenter(knockRequestsFlow = knockRequests) + presenter.test { + skipItems(2) + awaitItem().also { state -> + state.eventSink(KnockRequestsBannerEvents.Dismiss) + } + advanceUntilIdle() + assert(markAsSeenLambda).isCalledExactly(3) + } + } +} + +private fun TestScope.createKnockRequestsBannerPresenter( + knockRequestsFlow: Flow> = flowOf(emptyList()), + canAcceptKnockRequests: Boolean = true, + isFeatureEnabled: Boolean = true, +): KnockRequestsBannerPresenter { + val knockRequestsService = KnockRequestsService( + knockRequestsFlow = knockRequestsFlow, + coroutineScope = backgroundScope + ) + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.Knock.key to isFeatureEnabled + ) + ) + return KnockRequestsBannerPresenter( + room = FakeMatrixRoom( + canInviteResult = { Result.success(canAcceptKnockRequests) } + ), + knockRequestsService = knockRequestsService, + appCoroutineScope = this, + featureFlagService = featureFlagService + ) +} diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt new file mode 100644 index 0000000000..7326be47b2 --- /dev/null +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt @@ -0,0 +1,102 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +package io.element.android.features.knockrequests.impl.banner + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.knockrequests.impl.R +import io.element.android.features.knockrequests.impl.data.aKnockRequest +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class KnockRequestsListViewTest { + @get:Rule + val rule = createAndroidComposeRule() + + @Test + fun `clicking on view on single request invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setKnockRequestsBannerView( + state = aKnockRequestsBannerState( + eventSink = eventsRecorder, + ), + onViewRequestsClick = it + ) + rule.clickOn(R.string.screen_room_single_knock_request_view_button_title) + } + } + + @Test + fun `clicking on view all when multiple requests invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setKnockRequestsBannerView( + state = aKnockRequestsBannerState( + knockRequests = listOf( + aKnockRequest(displayName = "Alice"), + aKnockRequest(displayName = "Bob"), + aKnockRequest(displayName = "Charlie") + ), + eventSink = eventsRecorder, + ), + onViewRequestsClick = it + ) + rule.clickOn(R.string.screen_room_multiple_knock_requests_view_all_button_title) + } + } + + @Test + fun `clicking on accept on a single request emit the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setKnockRequestsBannerView( + state = aKnockRequestsBannerState( + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_accept) + eventsRecorder.assertSingle(KnockRequestsBannerEvents.AcceptSingleRequest) + } + + @Test + fun `clicking on dismiss emit the expected event`() { + val eventsRecorder = EventsRecorder() + rule.setKnockRequestsBannerView( + state = aKnockRequestsBannerState( + eventSink = eventsRecorder, + ), + ) + val close = rule.activity.getString(CommonStrings.action_close) + rule.onNodeWithContentDescription(close).performClick() + eventsRecorder.assertSingle(KnockRequestsBannerEvents.Dismiss) + } +} + +private fun AndroidComposeTestRule.setKnockRequestsBannerView( + state: KnockRequestsBannerState, + onViewRequestsClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + KnockRequestsBannerView( + state = state, + onViewRequestsClick = onViewRequestsClick, + ) + } +} diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt new file mode 100644 index 0000000000..1638428c79 --- /dev/null +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -0,0 +1,310 @@ +/* + * Copyright 2024 New Vector Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only + * Please see LICENSE in the repository root for full details. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.knockrequests.impl.list + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.knockrequests.impl.data.KnockRequestsService +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.matrix.api.room.knock.KnockRequest +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest +import io.element.android.tests.testutils.lambda.assert +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.test +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class KnockRequestsListPresenterTest { + @Test + fun `present - initial states should be emitted`() = runTest { + val presenter = createKnockRequestsListPresenter() + presenter.test { + awaitItem().also { state -> + assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java) + assertThat(state.canAccept).isFalse() + assertThat(state.canDecline).isFalse() + assertThat(state.canBan).isFalse() + } + awaitItem().also { state -> + assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java) + assertThat(state.canAccept).isTrue() + assertThat(state.canDecline).isTrue() + assertThat(state.canBan).isTrue() + } + awaitItem().also { state -> + assertThat(state.knockRequests).isInstanceOf(AsyncData.Success::class.java) + assertThat(state.knockRequests.dataOrNull()).isEmpty() + } + } + } + + @Test + fun `present - accept success scenario`() = runTest { + val acceptLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable)) + } + skipItems(1) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable)) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() + } + assert(acceptLambda).isCalledOnce() + } + } + + @Test + fun `present - accept failure scenario`() = runTest { + val acceptLambda = lambdaRecorder> { Result.failure(Exception()) } + val knockRequest = FakeKnockRequest(acceptLambda = acceptLambda) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + state.eventSink(KnockRequestsListEvents.Accept(knockRequestPresentable)) + } + skipItems(1) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable)) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(KnockRequestsListEvents.RetryCurrentAction) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull()).hasSize(1) + } + assert(acceptLambda).isCalledExactly(2) + } + } + + @Test + fun `present - decline success scenario`() = runTest { + val declineLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequest = FakeKnockRequest(declineLambda = declineLambda) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + state.eventSink(KnockRequestsListEvents.Decline(knockRequestPresentable)) + } + skipItems(1) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Decline(knockRequestPresentable)) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) + state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() + } + } + assert(declineLambda).isCalledOnce() + } + + @Test + fun `present - decline and ban success scenario`() = runTest { + val declineAndBanLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequest = FakeKnockRequest(declineAndBanLambda = declineAndBanLambda) + val knockRequests = flowOf(listOf(knockRequest)) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + state.eventSink(KnockRequestsListEvents.DeclineAndBan(knockRequestPresentable)) + } + skipItems(1) + awaitItem().also { state -> + val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.DeclineAndBan(knockRequestPresentable)) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) + state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() + } + } + assert(declineAndBanLambda).isCalledOnce() + } + + @Test + fun `present - accept all success scenario`() = runTest { + val acceptLambda = lambdaRecorder> { Result.success(Unit) } + val knockRequests = flowOf( + listOf( + FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptLambda), + FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptLambda), + ) + ) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.canAcceptAll).isTrue() + state.eventSink(KnockRequestsListEvents.AcceptAll) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) + state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Success::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() + } + } + assert(acceptLambda).isCalledExactly(2) + } + + @Test + fun `present - accept all partial success scenario`() = runTest { + val acceptSuccessLambda = lambdaRecorder> { Result.success(Unit) } + val acceptFailureLambda = lambdaRecorder> { Result.failure(Exception()) } + val knockRequests = flowOf( + listOf( + FakeKnockRequest(eventId = AN_EVENT_ID, acceptLambda = acceptSuccessLambda), + FakeKnockRequest(eventId = AN_EVENT_ID_2, acceptLambda = acceptFailureLambda), + ) + ) + val presenter = createKnockRequestsListPresenter( + knockRequestsFlow = knockRequests + ) + presenter.test { + skipItems(2) + awaitItem().also { state -> + assertThat(state.canAcceptAll).isTrue() + state.eventSink(KnockRequestsListEvents.AcceptAll) + } + skipItems(1) + awaitItem().also { state -> + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll) + assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) + state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) + } + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Failure::class.java) + state.eventSink(KnockRequestsListEvents.ResetCurrentAction) + } + skipItems(2) + awaitItem().also { state -> + assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) + assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.knockRequests.dataOrNull()).hasSize(1) + } + } + assert(acceptFailureLambda).isCalledOnce() + assert(acceptSuccessLambda).isCalledOnce() + } + + private fun TestScope.createKnockRequestsListPresenter( + canAccept: Boolean = true, + canDecline: Boolean = true, + canBan: Boolean = true, + knockRequestsFlow: Flow> = flowOf(emptyList()) + ): KnockRequestsListPresenter { + val room = FakeMatrixRoom( + canInviteResult = { Result.success(canAccept) }, + canKickResult = { Result.success(canDecline) }, + canBanResult = { Result.success(canBan) } + ) + val knockRequestsService = KnockRequestsService( + knockRequestsFlow = knockRequestsFlow, + coroutineScope = backgroundScope + ) + return KnockRequestsListPresenter( + room = room, + knockRequestsService = knockRequestsService, + ) + } +} diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt new file mode 100644 index 0000000000..5620c65ea7 --- /dev/null +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt @@ -0,0 +1,163 @@ +/* + * 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.list + +import androidx.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.features.knockrequests.impl.R +import io.element.android.features.knockrequests.impl.data.aKnockRequest +import io.element.android.libraries.architecture.AsyncAction +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.pressBack +import kotlinx.collections.immutable.persistentListOf +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class KnockRequestsListViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on back invoke the expected callback`() { + val eventsRecorder = EventsRecorder(expectEvents = false) + ensureCalledOnce { + rule.setKnockRequestsListView( + aKnockRequestsListState( + eventSink = eventsRecorder, + ), + onBackClick = it + ) + rule.pressBack() + } + } + + @Test + fun `clicking on accept emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequest = aKnockRequest() + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(persistentListOf(knockRequest)), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_accept) + eventsRecorder.assertSingle(KnockRequestsListEvents.Accept(knockRequest)) + } + + @Test + fun `clicking on decline emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequest = aKnockRequest() + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(persistentListOf(knockRequest)), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_decline) + eventsRecorder.assertSingle(KnockRequestsListEvents.Decline(knockRequest)) + } + + @Test + fun `clicking on decline and ban emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequest = aKnockRequest() + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(persistentListOf(knockRequest)), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_knock_requests_list_decline_and_ban_action_title) + eventsRecorder.assertSingle(KnockRequestsListEvents.DeclineAndBan(knockRequest)) + } + + @Test + fun `clicking on accept all emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(knockRequests), + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_knock_requests_list_accept_all_button_title) + eventsRecorder.assertSingle(KnockRequestsListEvents.AcceptAll) + } + + @Test + fun `retry on async view retry emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(knockRequests), + asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), + actionTarget = KnockRequestsActionTarget.AcceptAll, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_retry) + eventsRecorder.assertSingle(KnockRequestsListEvents.RetryCurrentAction) + } + + @Test + fun `canceling async view emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(knockRequests), + asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), + actionTarget = KnockRequestsActionTarget.AcceptAll, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(CommonStrings.action_cancel) + eventsRecorder.assertSingle(KnockRequestsListEvents.ResetCurrentAction) + } + + @Test + fun `confirming async view emit the expected event`() { + val eventsRecorder = EventsRecorder() + val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + rule.setKnockRequestsListView( + aKnockRequestsListState( + knockRequests = AsyncData.Success(knockRequests), + asyncAction = AsyncAction.ConfirmingNoParams, + actionTarget = KnockRequestsActionTarget.AcceptAll, + eventSink = eventsRecorder, + ), + ) + rule.clickOn(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title) + eventsRecorder.assertSingle(KnockRequestsListEvents.ConfirmCurrentAction) + } +} + +private fun AndroidComposeTestRule.setKnockRequestsListView( + state: KnockRequestsListState, + onBackClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + KnockRequestsListView( + state = state, + onBackClick = onBackClick, + ) + } +} diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt index fad12e6bb6..88866b9dde 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt @@ -7,37 +7,42 @@ package io.element.android.libraries.matrix.test.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.room.knock.KnockRequest import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.tests.testutils.lambda.lambdaError +import io.element.android.tests.testutils.simulateLongTask class FakeKnockRequest( + override val eventId: EventId = AN_EVENT_ID, override val userId: UserId = A_USER_ID, override val displayName: String? = A_USER_NAME, override val avatarUrl: String? = AN_AVATAR_URL, override val reason: String? = null, override val timestamp: Long? = null, + override val isSeen: Boolean = false, val acceptLambda: () -> Result = { lambdaError() }, val declineLambda: (String?) -> Result = { lambdaError() }, val declineAndBanLambda: (String?) -> Result = { lambdaError() }, val markAsSeenLambda: () -> Result = { lambdaError() }, ) : KnockRequest { - override suspend fun accept(): Result { - return acceptLambda() + override suspend fun accept(): Result = simulateLongTask{ + acceptLambda() } - override suspend fun decline(reason: String?): Result { - return declineLambda(reason) + override suspend fun decline(reason: String?): Result = simulateLongTask { + declineLambda(reason) } - override suspend fun declineAndBan(reason: String?): Result { - return declineAndBanLambda(reason) + override suspend fun declineAndBan(reason: String?): Result = simulateLongTask { + declineAndBanLambda(reason) } - override suspend fun markAsSeen(): Result { - return markAsSeenLambda() + override suspend fun markAsSeen(): Result = simulateLongTask { + markAsSeenLambda() } } From 1345c4ea0f35e73d3ef982003a9fa4a97b2a0379 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2024 18:04:30 +0100 Subject: [PATCH 10/18] knock requests : fix breaking api --- .../android/libraries/matrix/impl/room/RustMatrixRoom.kt | 8 ++++---- .../libraries/matrix/impl/room/knock/RustKnockRequest.kt | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 64c4ba72c9..646ab8290a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -78,8 +78,7 @@ import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.DateDividerMode import org.matrix.rustcomponents.sdk.IdentityStatusChangeListener -import org.matrix.rustcomponents.sdk.JoinRequest -import org.matrix.rustcomponents.sdk.JoinRequestsListener +import org.matrix.rustcomponents.sdk.KnockRequestsListener import org.matrix.rustcomponents.sdk.RoomInfo import org.matrix.rustcomponents.sdk.RoomInfoListener import org.matrix.rustcomponents.sdk.RoomListItem @@ -95,6 +94,7 @@ import uniffi.matrix_sdk.RoomPowerLevelChanges import java.io.File import kotlin.coroutines.cancellation.CancellationException import org.matrix.rustcomponents.sdk.IdentityStatusChange as RustIdentityStateChange +import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest import org.matrix.rustcomponents.sdk.Room as InnerRoom import org.matrix.rustcomponents.sdk.Timeline as InnerTimeline @@ -162,8 +162,8 @@ class RustMatrixRoom( } override val knockRequestsFlow: Flow> = mxCallbackFlow { - innerRoom.subscribeToJoinRequests(object : JoinRequestsListener { - override fun call(joinRequests: List) { + innerRoom.subscribeToKnockRequests(object : KnockRequestsListener { + override fun call(joinRequests: List) { val knockRequests = joinRequests.map { RustKnockRequest(it) } channel.trySend(knockRequests) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt index 8f7b075511..9e12866c9c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/knock/RustKnockRequest.kt @@ -10,10 +10,10 @@ 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.room.knock.KnockRequest -import org.matrix.rustcomponents.sdk.JoinRequest +import org.matrix.rustcomponents.sdk.KnockRequest as InnerKnockRequest class RustKnockRequest( - private val inner: JoinRequest, + private val inner: InnerKnockRequest, ) : KnockRequest { override val eventId: EventId = EventId(inner.eventId) override val userId: UserId = UserId(inner.userId) From a973c8f02866c1679e2f56bf793ee0ad9ebe9040 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2024 18:10:00 +0100 Subject: [PATCH 11/18] knock requests : fix wrong string resource for error --- .../knockrequests/impl/list/KnockRequestsListView.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index cee983050d..4857416f4e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -206,9 +206,9 @@ private fun KnockRequestsActionsView( errorMessage = { when (actionTarget) { is KnockRequestsActionTarget.Accept -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) - is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) - is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) - KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) + is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) + KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_failed_alert_description) else -> "" } }, From 7f372282dca198c93701f91caf2dabbac2d5ae4e Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2024 18:13:13 +0100 Subject: [PATCH 12/18] knock requests : format and clean --- .../impl/banner/KnockRequestsBannerState.kt | 2 +- .../impl/banner/KnockRequestsBannerView.kt | 2 -- .../impl/data/KnockRequestPresentable.kt | 1 - .../knockrequests/impl/data/KnockRequestsService.kt | 1 - .../knockrequests/impl/list/KnockRequestsListView.kt | 1 - .../impl/banner/KnockRequestsBannerPresenterTest.kt | 1 - .../impl/banner/KnockRequestsBannerViewTest.kt | 2 +- .../roomdetails/impl/RoomDetailsPresenter.kt | 2 -- .../libraries/matrix/api/room/join/JoinRule.kt | 12 ++++++------ .../matrix/impl/room/MatrixRoomInfoMapperTest.kt | 2 +- .../matrix/test/room/knock/FakeKnockRequest.kt | 2 +- 11 files changed, 10 insertions(+), 18 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt index 9d53181e82..80d662bc5b 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerState.kt @@ -10,8 +10,8 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.runtime.Composable import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.R +import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.libraries.core.extensions.firstIfSingle import kotlinx.collections.immutable.ImmutableList diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt index 69dfa268a6..d029b80906 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerView.kt @@ -25,7 +25,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset @@ -57,7 +56,6 @@ import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList -import timber.log.Timber private const val MAX_AVATAR_COUNT = 3 diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt index 6716908a56..5d45281d8a 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPresentable.kt @@ -32,5 +32,4 @@ interface KnockRequestPresentable { fun getBestName(): String { return displayName?.takeIf { it.isNotEmpty() } ?: userId.value } - } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index 5a373f3f36..298f4467e5 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -27,7 +27,6 @@ class KnockRequestsService( knockRequestsFlow: Flow>, coroutineScope: CoroutineScope, ) { - // 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>(emptySet()) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index 4857416f4e..3b7ef1670e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -229,7 +229,6 @@ private fun KnockRequestActionConfirmation( stringResource(R.string.screen_knock_requests_list_accept_all_alert_title), stringResource(R.string.screen_knock_requests_list_accept_all_alert_description), stringResource(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title), - ) is KnockRequestsActionTarget.Decline -> Triple( stringResource(R.string.screen_knock_requests_list_decline_alert_title), diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt index 189f392f95..01f91a81c4 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -29,7 +29,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class KnockRequestsBannerPresenterTest { - @Test fun `present - when feature is disabled then the banner should be hidden`() = runTest { val knockRequests = flowOf(listOf(FakeKnockRequest())) diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt index 7326be47b2..19716c349a 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerViewTest.kt @@ -26,7 +26,7 @@ import org.junit.rules.TestRule import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) -class KnockRequestsListViewTest { +class KnockRequestsBannerViewTest { @get:Rule val rule = createAndroidComposeRule() diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index 0fc907a867..e8af5a61a3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -50,7 +50,6 @@ import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -109,7 +108,6 @@ class RoomDetailsPresenter @Inject constructor( val roomType by getRoomType(dmMember, currentMember) val roomCallState = roomCallStatePresenter.present() - val topicState = remember(canEditTopic, roomTopic, roomType) { val topic = roomTopic when { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt index 33c2ccf092..b597fcf781 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/join/JoinRule.kt @@ -9,10 +9,10 @@ package io.element.android.libraries.matrix.api.room.join sealed interface JoinRule { data object Public : JoinRule - data object Private: JoinRule - data object Knock: JoinRule - data object Invite: JoinRule - data class Restricted(val rules: List): JoinRule - data class KnockRestricted(val rules: List): JoinRule - data class Custom(val value: String): JoinRule + data object Private : JoinRule + data object Knock : JoinRule + data object Invite : JoinRule + data class Restricted(val rules: List) : JoinRule + data class KnockRestricted(val rules: List) : JoinRule + data class Custom(val value: String) : JoinRule } diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt index 9f91ed16de..f277b26bae 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/room/MatrixRoomInfoMapperTest.kt @@ -31,8 +31,8 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableMap import kotlinx.collections.immutable.toPersistentList import org.junit.Test -import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.Membership +import org.matrix.rustcomponents.sdk.JoinRule as RustJoinRule import org.matrix.rustcomponents.sdk.RoomNotificationMode as RustRoomNotificationMode class MatrixRoomInfoMapperTest { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt index 88866b9dde..416feae8b8 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/knock/FakeKnockRequest.kt @@ -30,7 +30,7 @@ class FakeKnockRequest( val declineAndBanLambda: (String?) -> Result = { lambdaError() }, val markAsSeenLambda: () -> Result = { lambdaError() }, ) : KnockRequest { - override suspend fun accept(): Result = simulateLongTask{ + override suspend fun accept(): Result = simulateLongTask { acceptLambda() } From 80ae08648ac09311064416192419a1dc3304f48b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2024 20:24:29 +0100 Subject: [PATCH 13/18] knock requests : rework knock requests service to avoid reloading of data (and weird ui glitch because of them) --- .../banner/KnockRequestsBannerPresenter.kt | 16 ++------ .../impl/data/KnockRequestPermissions.kt | 32 ++++++++++++++++ .../impl/data/KnockRequestWrapper.kt | 24 ++++++------ .../impl/data/KnockRequestsModule.kt | 11 +++++- .../impl/data/KnockRequestsService.kt | 32 ++++++++++------ .../impl/list/KnockRequestsListPresenter.kt | 15 +------- .../impl/list/KnockRequestsListState.kt | 7 ++-- .../list/KnockRequestsListStateProvider.kt | 38 +++++++++++++------ .../impl/list/KnockRequestsListView.kt | 6 +-- .../KnockRequestsBannerPresenterTest.kt | 17 ++------- .../list/KnockRequestsListPresenterTest.kt | 28 ++++++-------- .../roomdetails/impl/RoomDetailsPresenter.kt | 1 - 12 files changed, 127 insertions(+), 100 deletions(-) create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt index a1ad661b0c..f155cdb4a3 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenter.kt @@ -19,11 +19,6 @@ import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.coroutine.mapState import io.element.android.libraries.core.extensions.firstIfSingle -import io.element.android.libraries.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState -import io.element.android.libraries.matrix.ui.room.canInviteAsState import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay @@ -33,10 +28,8 @@ import javax.inject.Inject private const val ACCEPT_ERROR_DISPLAY_DURATION = 1500L class KnockRequestsBannerPresenter @Inject constructor( - private val room: MatrixRoom, private val knockRequestsService: KnockRequestsService, private val appCoroutineScope: CoroutineScope, - private val featureFlagService: FeatureFlagService, ) : Presenter { @Composable override fun present(): KnockRequestsBannerState { @@ -48,15 +41,12 @@ class KnockRequestsBannerPresenter @Inject constructor( } }.collectAsState() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canAccept by room.canInviteAsState(syncUpdateFlow.value) - val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value) + val permissions by knockRequestsService.permissionsFlow.collectAsState() val showAcceptError = remember { mutableStateOf(false) } - val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false) val shouldShowBanner by remember { derivedStateOf { - isKnockRequestsEnabled && canHandleKnockRequests && knockRequests.isNotEmpty() + permissions.canHandle && knockRequests.isNotEmpty() } } @@ -79,7 +69,7 @@ class KnockRequestsBannerPresenter @Inject constructor( return KnockRequestsBannerState( knockRequests = knockRequests, displayAcceptError = showAcceptError.value, - canAccept = canAccept, + canAccept = permissions.canAccept, isVisible = shouldShowBanner, eventSink = ::handleEvents, ) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt new file mode 100644 index 0000000000..658717d48b --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestPermissions.kt @@ -0,0 +1,32 @@ +/* + * 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.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.powerlevels.canBan +import io.element.android.libraries.matrix.api.room.powerlevels.canInvite +import io.element.android.libraries.matrix.api.room.powerlevels.canKick +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +data class KnockRequestPermissions( + val canAccept: Boolean, + val canDecline: Boolean, + val canBan: Boolean, +) { + val canHandle = canAccept || canDecline || canBan +} + +fun MatrixRoom.knockRequestPermissionsFlow(): Flow { + return syncUpdateFlow.map { + val canAccept = canInvite().getOrDefault(false) + val canDecline = canKick().getOrDefault(false) + val canBan = canBan().getOrDefault(false) + KnockRequestPermissions(canAccept, canDecline, canBan) + } +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt index 56bcc34422..f1df84beab 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestWrapper.kt @@ -12,23 +12,23 @@ 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, + private val inner: 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) + override val eventId: EventId = inner.eventId + override val userId: UserId = inner.userId + override val displayName: String? = inner.displayName + override val avatarUrl: String? = inner.avatarUrl + override val reason: String? = inner.reason?.trim() + override val formattedDate: String? = dateFormatter(inner.timestamp) - val isSeen: Boolean = knockRequest.isSeen + val isSeen: Boolean = inner.isSeen - suspend fun accept(): Result = knockRequest.accept() + suspend fun accept(): Result = inner.accept() - suspend fun decline(reason: String?): Result = knockRequest.decline(reason) + suspend fun decline(reason: String?): Result = inner.decline(reason) - suspend fun declineAndBan(reason: String?): Result = knockRequest.declineAndBan(reason) + suspend fun declineAndBan(reason: String?): Result = inner.declineAndBan(reason) - suspend fun markAsSeen(): Result = knockRequest.markAsSeen() + suspend fun markAsSeen(): Result = inner.markAsSeen() } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt index 83f0a08c5b..1c1a17767f 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsModule.kt @@ -12,6 +12,8 @@ import dagger.Module import dagger.Provides import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom @Module @@ -19,7 +21,12 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom object KnockRequestsModule { @Provides @SingleIn(RoomScope::class) - fun knockRequestsService(room: MatrixRoom): KnockRequestsService { - return KnockRequestsService(room.knockRequestsFlow, room.roomCoroutineScope) + fun knockRequestsService(room: MatrixRoom, featureFlagService: FeatureFlagService): KnockRequestsService { + return KnockRequestsService( + knockRequestsFlow = room.knockRequestsFlow, + permissionsFlow = room.knockRequestPermissionsFlow(), + isKnockFeatureEnabledFlow = featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock), + coroutineScope = room.roomCoroutineScope + ) } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index 298f4467e5..d8a594f148 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -10,6 +10,7 @@ package io.element.android.features.knockrequests.impl.data import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.knock.KnockRequest +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async @@ -19,27 +20,40 @@ 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 class KnockRequestsService( knockRequestsFlow: Flow>, + permissionsFlow: Flow, + isKnockFeatureEnabledFlow: Flow, coroutineScope: CoroutineScope, ) { // 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>(emptySet()) val knockRequestsFlow = combine( - knockRequestsFlow.wrapped(), + isKnockFeatureEnabledFlow, + knockRequestsFlow, handledKnockRequestIds, - ) { knockRequests, handledKnockIds -> - val presentableKnockRequests = knockRequests - .filter { it.eventId !in handledKnockIds } - .toImmutableList() - AsyncData.Success(presentableKnockRequests) + ) { isKnockEnabled, knockRequests, handledKnockIds -> + if (!isKnockEnabled) { + AsyncData.Success(persistentListOf()) + } else { + val presentableKnockRequests = knockRequests + .filter { it.eventId !in handledKnockIds } + .map { inner -> KnockRequestWrapper(inner) } + .toImmutableList() + AsyncData.Success(presentableKnockRequests) + } }.stateIn(coroutineScope, SharingStarted.Lazily, AsyncData.Loading()) + val permissionsFlow = permissionsFlow.stateIn( + scope = coroutineScope, + started = SharingStarted.Lazily, + initialValue = KnockRequestPermissions(canAccept = false, canDecline = false, canBan = false) + ) + private fun knockRequestsList() = knockRequestsFlow.value.dataOrNull().orEmpty() private fun getKnockRequestById(eventId: EventId): KnockRequestWrapper? { @@ -128,8 +142,4 @@ class KnockRequestsService( } private fun knockRequestNotFoundResult() = Result.failure(IllegalArgumentException("Knock request not found")) - - private fun Flow>.wrapped() = map { knockRequests -> - knockRequests.map { KnockRequestWrapper(it) } - } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index d6e2adbebd..13c63d2d02 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -20,16 +20,11 @@ import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.AsyncAction 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.ui.room.canBanAsState -import io.element.android.libraries.matrix.ui.room.canInviteAsState -import io.element.android.libraries.matrix.ui.room.canKickAsState import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject class KnockRequestsListPresenter @Inject constructor( - private val room: MatrixRoom, private val knockRequestsService: KnockRequestsService, ) : Presenter { @Composable @@ -37,11 +32,7 @@ class KnockRequestsListPresenter @Inject constructor( val asyncAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } var actionTarget by remember { mutableStateOf(KnockRequestsActionTarget.None) } - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() - val canBan by room.canBanAsState(syncUpdateFlow.value) - val canDecline by room.canKickAsState(syncUpdateFlow.value) - val canAccept by room.canInviteAsState(syncUpdateFlow.value) - + val permissions by knockRequestsService.permissionsFlow.collectAsState() val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState() val coroutineScope = rememberCoroutineScope() @@ -79,10 +70,8 @@ class KnockRequestsListPresenter @Inject constructor( return KnockRequestsListState( knockRequests = knockRequests, actionTarget = actionTarget, + permissions = permissions, asyncAction = asyncAction.value, - canAccept = canAccept, - canDecline = canDecline, - canBan = canBan, eventSink = ::handleEvents ) } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt index 763305eca2..3447034afd 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt @@ -8,6 +8,7 @@ package io.element.android.features.knockrequests.impl.list import androidx.compose.runtime.Immutable +import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData @@ -17,12 +18,10 @@ data class KnockRequestsListState( val knockRequests: AsyncData>, val actionTarget: KnockRequestsActionTarget, val asyncAction: AsyncAction, - val canAccept: Boolean, - val canDecline: Boolean, - val canBan: Boolean, + val permissions: KnockRequestPermissions, val eventSink: (KnockRequestsListEvents) -> Unit, ) { - val canAcceptAll = canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1 + val canAcceptAll = permissions.canAccept && knockRequests is AsyncData.Success && knockRequests.data.size > 1 } @Immutable diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index e7207cced1..f6d5aba7f6 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -8,6 +8,7 @@ package io.element.android.features.knockrequests.impl.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable import io.element.android.features.knockrequests.impl.data.aKnockRequest import io.element.android.libraries.architecture.AsyncAction @@ -82,7 +83,11 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider> = AsyncData.Success(persistentListOf()), actionTarget: KnockRequestsActionTarget = KnockRequestsActionTarget.None, asyncAction: AsyncAction = AsyncAction.Uninitialized, - canAccept: Boolean = true, - canDecline: Boolean = true, - canBan: Boolean = true, + permissions: KnockRequestPermissions = KnockRequestPermissions( + canAccept = true, + canDecline = true, + canBan = true, + ), eventSink: (KnockRequestsListEvents) -> Unit = {}, ) = KnockRequestsListState( knockRequests = knockRequests, actionTarget = actionTarget, asyncAction = asyncAction, - canAccept = canAccept, - canDecline = canDecline, - canBan = canBan, + permissions = permissions, eventSink = eventSink, ) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index 3b7ef1670e..2606bf26c4 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -126,9 +126,9 @@ private fun KnockRequestsListContent( } else { KnockRequestsList( knockRequests = knockRequests, - canAccept = state.canAccept, - canDecline = state.canDecline, - canBan = state.canBan, + canAccept = state.permissions.canAccept, + canDecline = state.permissions.canDecline, + canBan = state.permissions.canBan, onAcceptClick = ::onAcceptClick, onDeclineClick = ::onDeclineClick, onBanClick = ::onBanClick, diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt index 01f91a81c4..f692c85aa7 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -8,14 +8,12 @@ package io.element.android.features.knockrequests.impl.banner import com.google.common.truth.Truth.assertThat +import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.impl.data.KnockRequestsService -import io.element.android.libraries.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_ID_2 import io.element.android.libraries.matrix.test.A_USER_ID_3 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -234,19 +232,12 @@ private fun TestScope.createKnockRequestsBannerPresenter( ): KnockRequestsBannerPresenter { val knockRequestsService = KnockRequestsService( knockRequestsFlow = knockRequestsFlow, - coroutineScope = backgroundScope - ) - val featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.Knock.key to isFeatureEnabled - ) + coroutineScope = backgroundScope, + isKnockFeatureEnabledFlow = flowOf(isFeatureEnabled), + permissionsFlow = flowOf(KnockRequestPermissions(canAcceptKnockRequests, canAcceptKnockRequests, canAcceptKnockRequests)), ) return KnockRequestsBannerPresenter( - room = FakeMatrixRoom( - canInviteResult = { Result.success(canAcceptKnockRequests) } - ), knockRequestsService = knockRequestsService, appCoroutineScope = this, - featureFlagService = featureFlagService ) } diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt index 1638428c79..010a921515 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -10,13 +10,13 @@ package io.element.android.features.knockrequests.impl.list import com.google.common.truth.Truth.assertThat +import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.impl.data.KnockRequestsService import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.room.knock.KnockRequest import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.knock.FakeKnockRequest import io.element.android.tests.testutils.lambda.assert import io.element.android.tests.testutils.lambda.lambdaRecorder @@ -35,15 +35,15 @@ class KnockRequestsListPresenterTest { presenter.test { awaitItem().also { state -> assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java) - assertThat(state.canAccept).isFalse() - assertThat(state.canDecline).isFalse() - assertThat(state.canBan).isFalse() + assertThat(state.permissions.canAccept).isFalse() + assertThat(state.permissions.canDecline).isFalse() + assertThat(state.permissions.canBan).isFalse() } awaitItem().also { state -> assertThat(state.knockRequests).isInstanceOf(AsyncData.Loading::class.java) - assertThat(state.canAccept).isTrue() - assertThat(state.canDecline).isTrue() - assertThat(state.canBan).isTrue() + assertThat(state.permissions.canAccept).isTrue() + assertThat(state.permissions.canDecline).isTrue() + assertThat(state.permissions.canBan).isTrue() } awaitItem().also { state -> assertThat(state.knockRequests).isInstanceOf(AsyncData.Success::class.java) @@ -293,18 +293,12 @@ class KnockRequestsListPresenterTest { canBan: Boolean = true, knockRequestsFlow: Flow> = flowOf(emptyList()) ): KnockRequestsListPresenter { - val room = FakeMatrixRoom( - canInviteResult = { Result.success(canAccept) }, - canKickResult = { Result.success(canDecline) }, - canBanResult = { Result.success(canBan) } - ) val knockRequestsService = KnockRequestsService( knockRequestsFlow = knockRequestsFlow, - coroutineScope = backgroundScope - ) - return KnockRequestsListPresenter( - room = room, - knockRequestsService = knockRequestsService, + coroutineScope = backgroundScope, + isKnockFeatureEnabledFlow = flowOf(true), + permissionsFlow = flowOf(KnockRequestPermissions(canAccept, canDecline, canBan)), ) + return KnockRequestsListPresenter(knockRequestsService = knockRequestsService) } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt index e8af5a61a3..50c403fe87 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsPresenter.kt @@ -48,7 +48,6 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.analyticsproviders.api.trackers.captureInteraction import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch From 9f0847f211bb1cfac9c4f22196459a7877f714a1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 18 Dec 2024 20:36:40 +0100 Subject: [PATCH 14/18] knock requests : fix test name --- .../impl/banner/KnockRequestsBannerPresenterTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt index f692c85aa7..7027061804 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerPresenterTest.kt @@ -62,7 +62,7 @@ import org.junit.Test } @Test - fun `present - when everything is setup to manage knocks with data, then the banner should be visible `() = runTest { + fun `present - when everything is setup to manage knocks with data, then the banner should be visible`() = runTest { val knockRequests = flowOf( listOf( FakeKnockRequest( From eb9b6a72ee635a4a64bf56619d23a5531507491d Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 18 Dec 2024 19:48:24 +0000 Subject: [PATCH 15/18] Update screenshots --- ...krequests.impl.banner_KnockRequestsBannerView_Day_0_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_1_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_2_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_4_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_5_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_6_en.png | 4 ++-- ...krequests.impl.banner_KnockRequestsBannerView_Day_7_en.png | 3 --- ...equests.impl.banner_KnockRequestsBannerView_Night_0_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_1_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_2_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_4_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_5_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_6_en.png | 4 ++-- ...equests.impl.banner_KnockRequestsBannerView_Night_7_en.png | 3 --- ...knockrequests.impl.list_KnockRequestsListView_Day_0_en.png | 4 ++-- ...nockrequests.impl.list_KnockRequestsListView_Day_10_en.png | 3 +++ ...knockrequests.impl.list_KnockRequestsListView_Day_5_en.png | 4 ++-- ...knockrequests.impl.list_KnockRequestsListView_Day_6_en.png | 4 ++-- ...knockrequests.impl.list_KnockRequestsListView_Day_7_en.png | 4 ++-- ...knockrequests.impl.list_KnockRequestsListView_Day_8_en.png | 4 ++-- ...knockrequests.impl.list_KnockRequestsListView_Day_9_en.png | 4 ++-- ...ockrequests.impl.list_KnockRequestsListView_Night_0_en.png | 4 ++-- ...ckrequests.impl.list_KnockRequestsListView_Night_10_en.png | 3 +++ ...ockrequests.impl.list_KnockRequestsListView_Night_5_en.png | 4 ++-- ...ockrequests.impl.list_KnockRequestsListView_Night_6_en.png | 4 ++-- ...ockrequests.impl.list_KnockRequestsListView_Night_7_en.png | 4 ++-- ...ockrequests.impl.list_KnockRequestsListView_Night_8_en.png | 4 ++-- ...ockrequests.impl.list_KnockRequestsListView_Night_9_en.png | 4 ++-- 28 files changed, 54 insertions(+), 54 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png index 8d01d01fbd..6866b6afc0 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77db58461ef35ea3d1ab1dda6aff454e376b15d7037e83fbbd965037afa45572 -size 29317 +oid sha256:74f93deb90501b746d95e0edf2ae2cef58036a388888e42a9b0fd8aadac9758c +size 29447 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png index 4441fd8ef3..9298210927 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:585ea7b1f230ac6a12f8098200b04f433cbb30b39b8be7953dc6e278ffe8179e -size 34799 +oid sha256:6a9a248862bd05327e10481f868c6dc10cc1ccad6a56284d497a9fb45f737206 +size 35042 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png index 75cafbe376..a5e66c073f 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:05770e4e11bbc7019ebc113d31eb8b76bbbddc8b1ca6acbb01c0764089047376 -size 17835 +oid sha256:ef72766061f056096fc6d030b0e12d05dfa767ea5cac45d71b02cdbd0e78971b +size 17859 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png index 55bf403a0d..8eef415284 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a10ccd7db4660bdff998622f5390a33adcaa39472c7d60ebae7c0a5b30a810d4 -size 27294 +oid sha256:3b98dc51e1f31bcd64dee223be7beb66b6ddd814189f290876633f5727d09f18 +size 27383 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png index 8d01d01fbd..462b3bc35e 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77db58461ef35ea3d1ab1dda6aff454e376b15d7037e83fbbd965037afa45572 -size 29317 +oid sha256:a386397f9a0f037050797980a3bd9482ec8a2f1a37d5e150ed8cdd50e5b576a1 +size 31873 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png index 8d01d01fbd..ab44725d4c 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77db58461ef35ea3d1ab1dda6aff454e376b15d7037e83fbbd965037afa45572 -size 29317 +oid sha256:1b0d57fed7af1716e23f9f1167f70199f5a21f60f3ac97940deccd824754a901 +size 39156 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_en.png deleted file mode 100644 index ab52d1d019..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Day_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:43e80c40ea0b0f6c02d03cf37537fa9e1b8ac71ec5f1d279b51477538a743063 -size 39209 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png index fd40cbce60..29ea2f7789 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f2c03edb4eaec4917b95955ac0b51cbdce25ae9710757cad68e3ed863707f7a -size 27374 +oid sha256:1a7c6898ea6667e5b7f09ee99ca6b2d75ae3bc20e0db57b9d7a20c48d4681dca +size 27308 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png index 8c1e0742e5..43684248f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3811a02bebe20381a5996bcb61176d258fbb1ca4302cf5f9742470c3f152c780 -size 32258 +oid sha256:e229ac5d069412a18ae7deeefd0e88d9897b239ab32a0edd2c2d06f139f1f1af +size 32430 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png index ca5bf4cb5f..31c1d7e220 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aaf05a0f912cedf6897fac21e679429b86b0ed1d3e8b6a07cfe7ec6f60cec13 -size 15885 +oid sha256:62cf8a40ea8670ce9a23923d5791137ec057b130cde637c4dfb5b0edc75d7971 +size 15926 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png index 38ace64640..02fd5af4b3 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17d3263b1c47a75083bd597f73baf8cf5faa74274810e85cf2ee086812ca0e60 -size 25248 +oid sha256:89fdc1cf49c756e40d81eb858b507f751a6ff661fcb4f3194a5d7c0f738c1b94 +size 25250 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png index fd40cbce60..6abdd712c0 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f2c03edb4eaec4917b95955ac0b51cbdce25ae9710757cad68e3ed863707f7a -size 27374 +oid sha256:ad9532960a251f04ec0fd513df561010a91923ab41de8318f99df23d3799feec +size 28232 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png index fd40cbce60..de9b3caf77 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f2c03edb4eaec4917b95955ac0b51cbdce25ae9710757cad68e3ed863707f7a -size 27374 +oid sha256:8f2419c455efb74f0f528017e411c16d76755646e3449ae4a62077857e3ea93b +size 36848 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_7_en.png deleted file mode 100644 index 81355d2bfc..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.banner_KnockRequestsBannerView_Night_7_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cbed125bd5c334bb5d7fce60df615d188c713989bcd6450f59d3f37973ab2bc7 -size 36735 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png index d2ddc2c7c6..8b36ceefb5 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fbbd687e5e0a1fd3a1be442937e32990dc72a6e4817d6ffc29f6f596eeddef1b -size 8086 +oid sha256:0132466eb104c2249958a310b19336f97a5485f98e47a233c29740b14b9907ee +size 14298 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png new file mode 100644 index 0000000000..8f0414d4b3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9afb58cfcd13c064f0b17d449318d5111aaa914cb8875fe0596c66e5a8a17051 +size 30647 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png index c14ef5dd52..9463263a2f 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7e66d3f7d10e759f629795580545cda6644292020ce36763d07e9d476dcc231 -size 30325 +oid sha256:dcd89988740d57132796306568378471b67ceb7c182e82fce5fa01dc936a640f +size 44821 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png index 466e591450..a8e76f0ab7 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:586021e67718aa949b1de04799a43d9da8189fadacdb6dba23405c762fc7ee06 -size 30618 +oid sha256:7ee73fabc6f4db8b7fb26b606723c69cf169e3a003a828b74654b6286235e164 +size 34430 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png index 2952c5bd81..c4e4262ce2 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1b71dc6dc0d29801e81d1d4e52df811624583f69c0510f2f650676747a326455 -size 30296 +oid sha256:170e1d045120d172d26d5d7bdd6bbf42c1a5bddcad7ff38d5352cd38c05efed4 +size 39551 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png index 9c47af83a4..2952c5bd81 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f6be2cfe7991bce6c622f4f2e45f4ac96810cdc9462d9a4d7aea11baf69ab5ac -size 27446 +oid sha256:1b71dc6dc0d29801e81d1d4e52df811624583f69c0510f2f650676747a326455 +size 30296 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png index 8f0414d4b3..9c47af83a4 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9afb58cfcd13c064f0b17d449318d5111aaa914cb8875fe0596c66e5a8a17051 -size 30647 +oid sha256:f6be2cfe7991bce6c622f4f2e45f4ac96810cdc9462d9a4d7aea11baf69ab5ac +size 27446 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png index 5188e40e69..5ea469c53f 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ff4bc9e0588ea6a8396572b9ac9fb2401c3193fc3f1fbeb75efd69be6dda24d -size 7867 +oid sha256:ab53aa77207ee6984eb5a7a619b5b748178819bd2006ace4282f28f1c6b16cac +size 13944 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png new file mode 100644 index 0000000000..54a5fc7d9c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_10_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:68294db4cbd8bb42f4a0b6534b48585341beb2bc405e93cf74a2655f049522ff +size 29768 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png index 45b2a829ef..6fec3fbf9a 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c30b6e25ec148d4f98a6c305b7d71af6c7e413cc3425f5a6f5b42a201039f491 -size 29007 +oid sha256:00c25643adf7f06ef46e824aa60b35348c18e596a82557b1546b55d29392aa11 +size 42613 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png index b39ccb0150..d626612346 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5c6abc8bd5ca40eb91d926adc798cee95a8aacb0e87d937983c6240173fe2b9 -size 30071 +oid sha256:ad43b5aaa3ea7cf3ab79af88a2a93ba4294527ad2c40c72f4312f3e1b92b4f1a +size 33280 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png index 215cef276c..3cbbf256ec 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3b67c8d2cedb724ece75a2171463c4e035d562c4480d39b11b1072f4fdae3053 -size 29840 +oid sha256:6d9f612c2e6d46ed59ec5a3d1c65fe8953e4fc8f0955bddaa2abef6e1eabb00d +size 37194 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png index 171b5f5aa5..215cef276c 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15903c9494100becc4dd2bc6e9fb7f38f8eebbb595cbc251cfc0f839d9a99a27 -size 27322 +oid sha256:3b67c8d2cedb724ece75a2171463c4e035d562c4480d39b11b1072f4fdae3053 +size 29840 diff --git a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png index 54a5fc7d9c..171b5f5aa5 100644 --- a/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.knockrequests.impl.list_KnockRequestsListView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68294db4cbd8bb42f4a0b6534b48585341beb2bc405e93cf74a2655f049522ff -size 29768 +oid sha256:15903c9494100becc4dd2bc6e9fb7f38f8eebbb595cbc251cfc0f839d9a99a27 +size 27322 From 189cc5c58eccd4438cb704cccaf0ecf986870f12 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2024 19:31:53 +0100 Subject: [PATCH 16/18] knock requests : add KnockRequestsException --- .../impl/data/KnockRequestsException.kt | 13 +++++++++++++ .../knockrequests/impl/data/KnockRequestsService.kt | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt new file mode 100644 index 0000000000..0880233ff6 --- /dev/null +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsException.kt @@ -0,0 +1,13 @@ +/* + * 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 + +sealed class KnockRequestsException : Exception() { + data object AcceptAllPartiallyFailed : KnockRequestsException() + data object KnockRequestNotFound : KnockRequestsException() +} diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt index d8a594f148..fb08821387 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestsService.kt @@ -105,7 +105,7 @@ class KnockRequestsService( if (results.all { it.isSuccess }) { Result.success(Unit) } else { - Result.failure(IllegalStateException("Failed to accept all knock requests")) + Result.failure(KnockRequestsException.AcceptAllPartiallyFailed) } } @@ -140,6 +140,6 @@ class KnockRequestsService( } } } - - private fun knockRequestNotFoundResult() = Result.failure(IllegalArgumentException("Knock request not found")) } + +private fun knockRequestNotFoundResult() = Result.failure(KnockRequestsException.KnockRequestNotFound) From 602b891bd0be5b1c0e7eba9057c2afca1a668b35 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2024 19:34:11 +0100 Subject: [PATCH 17/18] knock requests : rename fixture aKnockRequestPresentable --- .../KnockRequestsBannerStateProvider.kt | 20 ++++++++--------- .../impl/data/KnockRequestFixture.kt | 2 +- .../list/KnockRequestsListStateProvider.kt | 22 +++++++++---------- .../banner/KnockRequestsBannerViewTest.kt | 8 +++---- .../impl/list/KnockRequestsListViewTest.kt | 16 +++++++------- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt index 460d1c7462..0324239fd9 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/banner/KnockRequestsBannerStateProvider.kt @@ -9,7 +9,7 @@ package io.element.android.features.knockrequests.impl.banner import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable -import io.element.android.features.knockrequests.impl.data.aKnockRequest +import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import kotlinx.collections.immutable.toImmutableList class KnockRequestsBannerStateProvider : PreviewParameterProvider { @@ -18,7 +18,7 @@ class KnockRequestsBannerStateProvider : PreviewParameterProvider = listOf(aKnockRequest()), + knockRequests: List = listOf(aKnockRequestPresentable()), displayAcceptError: Boolean = false, canAccept: Boolean = true, isVisible: Boolean = true, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt index 5fbc2bb047..cfecb8355e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/data/KnockRequestFixture.kt @@ -10,7 +10,7 @@ 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( +fun aKnockRequestPresentable( eventId: EventId = EventId("\$eventId"), userId: UserId = UserId("@jacob_ross:example.com"), displayName: String? = "Jacob Ross", diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index f6d5aba7f6..7b5ee23353 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -10,7 +10,7 @@ package io.element.android.features.knockrequests.impl.list import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.knockrequests.impl.data.KnockRequestPermissions import io.element.android.features.knockrequests.impl.data.KnockRequestPresentable -import io.element.android.features.knockrequests.impl.data.aKnockRequest +import io.element.android.features.knockrequests.impl.data.aKnockRequestPresentable import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId @@ -31,14 +31,14 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider() - val knockRequest = aKnockRequest() + val knockRequest = aKnockRequestPresentable() rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), @@ -62,7 +62,7 @@ class KnockRequestsListViewTest { @Test fun `clicking on decline emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequest = aKnockRequest() + val knockRequest = aKnockRequestPresentable() rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), @@ -76,7 +76,7 @@ class KnockRequestsListViewTest { @Test fun `clicking on decline and ban emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequest = aKnockRequest() + val knockRequest = aKnockRequestPresentable() rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(persistentListOf(knockRequest)), @@ -90,7 +90,7 @@ class KnockRequestsListViewTest { @Test fun `clicking on accept all emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), @@ -104,7 +104,7 @@ class KnockRequestsListViewTest { @Test fun `retry on async view retry emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), @@ -120,7 +120,7 @@ class KnockRequestsListViewTest { @Test fun `canceling async view emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), @@ -136,7 +136,7 @@ class KnockRequestsListViewTest { @Test fun `confirming async view emit the expected event`() { val eventsRecorder = EventsRecorder() - val knockRequests = persistentListOf(aKnockRequest(), aKnockRequest()) + val knockRequests = persistentListOf(aKnockRequestPresentable(), aKnockRequestPresentable()) rule.setKnockRequestsListView( aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), From f4f669cbbbcd16a3daa73c45dde35c6d4aade3b9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 19 Dec 2024 20:08:14 +0100 Subject: [PATCH 18/18] knock requests : rename KnockRequestsActionTarget to KnockRequestAction --- .../impl/list/KnockRequestsListPresenter.kt | 42 +++++++++---------- .../impl/list/KnockRequestsListState.kt | 14 +++---- .../list/KnockRequestsListStateProvider.kt | 10 ++--- .../impl/list/KnockRequestsListView.kt | 42 +++++++++---------- .../list/KnockRequestsListPresenterTest.kt | 24 +++++------ .../impl/list/KnockRequestsListViewTest.kt | 6 +-- 6 files changed, 69 insertions(+), 69 deletions(-) diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt index 13c63d2d02..6ea13f16a1 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenter.kt @@ -30,7 +30,7 @@ class KnockRequestsListPresenter @Inject constructor( @Composable override fun present(): KnockRequestsListState { val asyncAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } - var actionTarget by remember { mutableStateOf(KnockRequestsActionTarget.None) } + var currentAction by remember { mutableStateOf(KnockRequestsAction.None) } val permissions by knockRequestsService.permissionsFlow.collectAsState() val knockRequests by knockRequestsService.knockRequestsFlow.collectAsState() @@ -40,36 +40,36 @@ class KnockRequestsListPresenter @Inject constructor( fun handleEvents(event: KnockRequestsListEvents) { when (event) { KnockRequestsListEvents.AcceptAll -> { - actionTarget = KnockRequestsActionTarget.AcceptAll + currentAction = KnockRequestsAction.AcceptAll } is KnockRequestsListEvents.Accept -> { - actionTarget = KnockRequestsActionTarget.Accept(event.knockRequest) + currentAction = KnockRequestsAction.Accept(event.knockRequest) } is KnockRequestsListEvents.Decline -> { - actionTarget = KnockRequestsActionTarget.Decline(event.knockRequest) + currentAction = KnockRequestsAction.Decline(event.knockRequest) } is KnockRequestsListEvents.DeclineAndBan -> { - actionTarget = KnockRequestsActionTarget.DeclineAndBan(event.knockRequest) + currentAction = KnockRequestsAction.DeclineAndBan(event.knockRequest) } KnockRequestsListEvents.ResetCurrentAction -> { asyncAction.value = AsyncAction.Uninitialized - actionTarget = KnockRequestsActionTarget.None + currentAction = KnockRequestsAction.None } KnockRequestsListEvents.RetryCurrentAction -> { - coroutineScope.executeAction(actionTarget, asyncAction, isActionConfirmed = true) + coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true) } KnockRequestsListEvents.ConfirmCurrentAction -> { - coroutineScope.executeAction(actionTarget, asyncAction, isActionConfirmed = true) + coroutineScope.executeAction(currentAction, asyncAction, isActionConfirmed = true) } } } - LaunchedEffect(actionTarget) { - executeAction(actionTarget, asyncAction, isActionConfirmed = false) + LaunchedEffect(currentAction) { + executeAction(currentAction, asyncAction, isActionConfirmed = false) } return KnockRequestsListState( knockRequests = knockRequests, - actionTarget = actionTarget, + currentAction = currentAction, permissions = permissions, asyncAction = asyncAction.value, eventSink = ::handleEvents @@ -77,35 +77,35 @@ class KnockRequestsListPresenter @Inject constructor( } private fun CoroutineScope.executeAction( - actionTarget: KnockRequestsActionTarget, + currentAction: KnockRequestsAction, asyncAction: MutableState>, isActionConfirmed: Boolean, ) = launch { - when (actionTarget) { - is KnockRequestsActionTarget.Accept -> { + when (currentAction) { + is KnockRequestsAction.Accept -> { runUpdatingState(asyncAction) { - knockRequestsService.acceptKnockRequest(actionTarget.knockRequest) + knockRequestsService.acceptKnockRequest(currentAction.knockRequest) } } - is KnockRequestsActionTarget.Decline -> { + is KnockRequestsAction.Decline -> { if (isActionConfirmed) { runUpdatingState(asyncAction) { - knockRequestsService.declineKnockRequest(actionTarget.knockRequest) + knockRequestsService.declineKnockRequest(currentAction.knockRequest) } } else { asyncAction.value = AsyncAction.ConfirmingNoParams } } - is KnockRequestsActionTarget.DeclineAndBan -> { + is KnockRequestsAction.DeclineAndBan -> { if (isActionConfirmed) { runUpdatingState(asyncAction) { - knockRequestsService.declineAndBanKnockRequest(actionTarget.knockRequest) + knockRequestsService.declineAndBanKnockRequest(currentAction.knockRequest) } } else { asyncAction.value = AsyncAction.ConfirmingNoParams } } - is KnockRequestsActionTarget.AcceptAll -> { + is KnockRequestsAction.AcceptAll -> { if (isActionConfirmed) { runUpdatingState(asyncAction) { knockRequestsService.acceptAllKnockRequests() @@ -114,7 +114,7 @@ class KnockRequestsListPresenter @Inject constructor( asyncAction.value = AsyncAction.ConfirmingNoParams } } - KnockRequestsActionTarget.None -> Unit + KnockRequestsAction.None -> Unit } } } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt index 3447034afd..fa33b074a5 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListState.kt @@ -16,7 +16,7 @@ import kotlinx.collections.immutable.ImmutableList data class KnockRequestsListState( val knockRequests: AsyncData>, - val actionTarget: KnockRequestsActionTarget, + val currentAction: KnockRequestsAction, val asyncAction: AsyncAction, val permissions: KnockRequestPermissions, val eventSink: (KnockRequestsListEvents) -> Unit, @@ -25,10 +25,10 @@ data class KnockRequestsListState( } @Immutable -sealed interface KnockRequestsActionTarget { - data object None : KnockRequestsActionTarget - data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget - data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget - data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsActionTarget - data object AcceptAll : KnockRequestsActionTarget +sealed interface KnockRequestsAction { + data object None : KnockRequestsAction + data class Accept(val knockRequest: KnockRequestPresentable) : KnockRequestsAction + data class Decline(val knockRequest: KnockRequestPresentable) : KnockRequestsAction + data class DeclineAndBan(val knockRequest: KnockRequestPresentable) : KnockRequestsAction + data object AcceptAll : KnockRequestsAction } diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt index 7b5ee23353..a8d898b08e 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListStateProvider.kt @@ -65,7 +65,7 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider> = AsyncData.Success(persistentListOf()), - actionTarget: KnockRequestsActionTarget = KnockRequestsActionTarget.None, + currentAction: KnockRequestsAction = KnockRequestsAction.None, asyncAction: AsyncAction = AsyncAction.Uninitialized, permissions: KnockRequestPermissions = KnockRequestPermissions( canAccept = true, @@ -142,7 +142,7 @@ fun aKnockRequestsListState( eventSink: (KnockRequestsListEvents) -> Unit = {}, ) = KnockRequestsListState( knockRequests = knockRequests, - actionTarget = actionTarget, + currentAction = currentAction, asyncAction = asyncAction, permissions = permissions, eventSink = eventSink, diff --git a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt index 2606bf26c4..09f916ae09 100644 --- a/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt +++ b/features/knockrequests/impl/src/main/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListView.kt @@ -153,7 +153,7 @@ private fun KnockRequestsListContent( else -> Unit } KnockRequestsActionsView( - actionTarget = state.actionTarget, + currentAction = state.currentAction, asyncAction = state.asyncAction, onConfirm = { state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) @@ -181,7 +181,7 @@ private fun KnockRequestsListContent( @Composable private fun KnockRequestsActionsView( - actionTarget: KnockRequestsActionTarget, + currentAction: KnockRequestsAction, asyncAction: AsyncAction, onConfirm: () -> Unit, onDismiss: () -> Unit, @@ -195,20 +195,20 @@ private fun KnockRequestsActionsView( onErrorDismiss = onDismiss, confirmationDialog = { KnockRequestActionConfirmation( - actionTarget = actionTarget, + currentAction = currentAction, onSubmit = onConfirm, onDismiss = onDismiss, ) }, progressDialog = { - KnockRequestActionProgress(target = actionTarget) + KnockRequestActionProgress(target = currentAction) }, errorMessage = { - when (actionTarget) { - is KnockRequestsActionTarget.Accept -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) - is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) - is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) - KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_failed_alert_description) + when (currentAction) { + is KnockRequestsAction.Accept -> stringResource(R.string.screen_knock_requests_list_accept_failed_alert_description) + is KnockRequestsAction.Decline -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) + is KnockRequestsAction.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_decline_failed_alert_description) + KnockRequestsAction.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_failed_alert_description) else -> "" } }, @@ -219,25 +219,25 @@ private fun KnockRequestsActionsView( @Composable private fun KnockRequestActionConfirmation( - actionTarget: KnockRequestsActionTarget, + currentAction: KnockRequestsAction, onSubmit: () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, ) { - val (title, content, submitText) = when (actionTarget) { - KnockRequestsActionTarget.AcceptAll -> Triple( + val (title, content, submitText) = when (currentAction) { + KnockRequestsAction.AcceptAll -> Triple( stringResource(R.string.screen_knock_requests_list_accept_all_alert_title), stringResource(R.string.screen_knock_requests_list_accept_all_alert_description), stringResource(R.string.screen_knock_requests_list_accept_all_alert_confirm_button_title), ) - is KnockRequestsActionTarget.Decline -> Triple( + is KnockRequestsAction.Decline -> Triple( stringResource(R.string.screen_knock_requests_list_decline_alert_title), - stringResource(R.string.screen_knock_requests_list_decline_alert_description, actionTarget.knockRequest.getBestName()), + stringResource(R.string.screen_knock_requests_list_decline_alert_description, currentAction.knockRequest.getBestName()), stringResource(R.string.screen_knock_requests_list_decline_alert_confirm_button_title), ) - is KnockRequestsActionTarget.DeclineAndBan -> Triple( + is KnockRequestsAction.DeclineAndBan -> Triple( stringResource(R.string.screen_knock_requests_list_ban_alert_title), - stringResource(R.string.screen_knock_requests_list_ban_alert_description, actionTarget.knockRequest.getBestName()), + stringResource(R.string.screen_knock_requests_list_ban_alert_description, currentAction.knockRequest.getBestName()), stringResource(R.string.screen_knock_requests_list_ban_alert_confirm_button_title), ) else -> return @@ -254,14 +254,14 @@ private fun KnockRequestActionConfirmation( @Composable private fun KnockRequestActionProgress( - target: KnockRequestsActionTarget, + target: KnockRequestsAction, modifier: Modifier = Modifier, ) { val progressText = when (target) { - is KnockRequestsActionTarget.Accept -> stringResource(R.string.screen_knock_requests_list_accept_loading_title) - is KnockRequestsActionTarget.Decline -> stringResource(R.string.screen_knock_requests_list_decline_loading_title) - is KnockRequestsActionTarget.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_ban_loading_title) - KnockRequestsActionTarget.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_loading_title) + is KnockRequestsAction.Accept -> stringResource(R.string.screen_knock_requests_list_accept_loading_title) + is KnockRequestsAction.Decline -> stringResource(R.string.screen_knock_requests_list_decline_loading_title) + is KnockRequestsAction.DeclineAndBan -> stringResource(R.string.screen_knock_requests_list_ban_loading_title) + KnockRequestsAction.AcceptAll -> stringResource(R.string.screen_knock_requests_list_accept_all_loading_title) else -> return } ProgressDialog( diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt index 010a921515..d74155ead1 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListPresenterTest.kt @@ -69,7 +69,7 @@ class KnockRequestsListPresenterTest { skipItems(1) awaitItem().also { state -> val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable)) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Accept(knockRequestPresentable)) assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) } awaitItem().also { state -> @@ -79,7 +79,7 @@ class KnockRequestsListPresenterTest { skipItems(2) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() } assert(acceptLambda).isCalledOnce() @@ -103,7 +103,7 @@ class KnockRequestsListPresenterTest { skipItems(1) awaitItem().also { state -> val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Accept(knockRequestPresentable)) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Accept(knockRequestPresentable)) assertThat(state.asyncAction).isInstanceOf(AsyncAction.Loading::class.java) } awaitItem().also { state -> @@ -120,7 +120,7 @@ class KnockRequestsListPresenterTest { skipItems(1) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull()).hasSize(1) } assert(acceptLambda).isCalledExactly(2) @@ -144,7 +144,7 @@ class KnockRequestsListPresenterTest { skipItems(1) awaitItem().also { state -> val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.Decline(knockRequestPresentable)) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.Decline(knockRequestPresentable)) assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) } @@ -158,7 +158,7 @@ class KnockRequestsListPresenterTest { skipItems(2) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() } } @@ -182,7 +182,7 @@ class KnockRequestsListPresenterTest { skipItems(1) awaitItem().also { state -> val knockRequestPresentable = state.knockRequests.dataOrNull()?.first()!! - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.DeclineAndBan(knockRequestPresentable)) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.DeclineAndBan(knockRequestPresentable)) assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) } @@ -196,7 +196,7 @@ class KnockRequestsListPresenterTest { skipItems(2) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() } } @@ -223,7 +223,7 @@ class KnockRequestsListPresenterTest { } skipItems(1) awaitItem().also { state -> - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll) assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) } @@ -237,7 +237,7 @@ class KnockRequestsListPresenterTest { skipItems(2) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull().orEmpty()).isEmpty() } } @@ -265,7 +265,7 @@ class KnockRequestsListPresenterTest { } skipItems(1) awaitItem().also { state -> - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.AcceptAll) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.AcceptAll) assertThat(state.asyncAction).isInstanceOf(AsyncAction.ConfirmingNoParams::class.java) state.eventSink(KnockRequestsListEvents.ConfirmCurrentAction) } @@ -279,7 +279,7 @@ class KnockRequestsListPresenterTest { skipItems(2) awaitItem().also { state -> assertThat(state.asyncAction).isInstanceOf(AsyncAction.Uninitialized::class.java) - assertThat(state.actionTarget).isEqualTo(KnockRequestsActionTarget.None) + assertThat(state.currentAction).isEqualTo(KnockRequestsAction.None) assertThat(state.knockRequests.dataOrNull()).hasSize(1) } } diff --git a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt index 2de89aef58..af2bfefd16 100644 --- a/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt +++ b/features/knockrequests/impl/src/test/kotlin/io/element/android/features/knockrequests/impl/list/KnockRequestsListViewTest.kt @@ -109,7 +109,7 @@ class KnockRequestsListViewTest { aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), - actionTarget = KnockRequestsActionTarget.AcceptAll, + currentAction = KnockRequestsAction.AcceptAll, eventSink = eventsRecorder, ), ) @@ -125,7 +125,7 @@ class KnockRequestsListViewTest { aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.Failure(Throwable("Failed to accept all")), - actionTarget = KnockRequestsActionTarget.AcceptAll, + currentAction = KnockRequestsAction.AcceptAll, eventSink = eventsRecorder, ), ) @@ -141,7 +141,7 @@ class KnockRequestsListViewTest { aKnockRequestsListState( knockRequests = AsyncData.Success(knockRequests), asyncAction = AsyncAction.ConfirmingNoParams, - actionTarget = KnockRequestsActionTarget.AcceptAll, + currentAction = KnockRequestsAction.AcceptAll, eventSink = eventsRecorder, ), )