Feature : Report room (#4654)
* feature (report room) : introduce all presentation classes. * feature (report room) : branch entry point in the room list * refactor (matrix ui) : move some code from appnav to matrix ui * feature (report room) : add api on room * feature (report room) : adjust ui * feature (report room) : branch api * feature (decline invite and block) : move things around and introduce presentation classes * feature (decline invite and block) : continue to move things * feature (report room) : remove reference to "conversation" for now * feature (report room) : add report room action to room detail screen * feature (report room) : enabled button state * feature (report room) : improve code and reuse * feature (report room) : add feature flag * feature (report room) : change feature flag to static bool * feature (report room) : add tests * feature (report room) : fix ui with new api on ListItem * feature (report room) : clean up and add more tests. * Update screenshots * feature (report room) : more test and fix issue * feature (report room) : update strings * feature (report room) : fix konsist preview * feature (report room) : disable feature * Update screenshots * var -> val * Improve preview of AcceptDeclineInviteView * Improve preview consistency * Add missing test on DismissErrorAndHideContent * Update screenshots * Add missing tests --------- Co-authored-by: ElementBot <android@element.io> Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
db6bc7c04f
commit
c2568f84d2
229 changed files with 3995 additions and 1210 deletions
|
|
@ -10,4 +10,7 @@ package io.element.android.appconfig
|
||||||
object MatrixConfiguration {
|
object MatrixConfiguration {
|
||||||
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
|
const val MATRIX_TO_PERMALINK_BASE_URL: String = "https://matrix.to/#/"
|
||||||
val clientPermalinkBaseUrl: String? = null
|
val clientPermalinkBaseUrl: String? = null
|
||||||
|
|
||||||
|
// TODO remove this when report is fixed
|
||||||
|
const val CAN_REPORT_ROOM = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomFlowNode
|
||||||
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
|
||||||
import io.element.android.appnav.room.joined.LoadingRoomNodeView
|
import io.element.android.appnav.room.joined.LoadingRoomNodeView
|
||||||
import io.element.android.appnav.room.joined.LoadingRoomState
|
|
||||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
import io.element.android.features.roomaliasesolver.api.RoomAliasResolverEntryPoint
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
|
|
@ -49,6 +48,7 @@ import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||||
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
import io.element.android.libraries.matrix.api.room.alias.ResolvedRoomAlias
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ import io.element.android.libraries.architecture.inputs
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncService
|
import io.element.android.libraries.matrix.api.sync.SyncService
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Text
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateProvider
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,6 @@ package io.element.android.appnav.room
|
||||||
|
|
||||||
import app.cash.turbine.test
|
import app.cash.turbine.test
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.appnav.room.joined.LoadingRoomState
|
|
||||||
import io.element.android.appnav.room.joined.LoadingRoomStateFlowFactory
|
|
||||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||||
|
|
@ -18,6 +16,8 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
import io.element.android.libraries.matrix.test.room.FakeJoinedRoom
|
||||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomState
|
||||||
|
import io.element.android.libraries.matrix.ui.room.LoadingRoomStateFlowFactory
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-compose-library")
|
id("io.element.android-compose-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
@ -16,5 +17,7 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.api
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.room.RoomInfo
|
||||||
|
import io.element.android.libraries.matrix.api.room.isDm
|
||||||
|
import io.element.android.libraries.matrix.api.room.preview.RoomPreviewInfo
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class InviteData(
|
||||||
|
val roomId: RoomId,
|
||||||
|
val roomName: String,
|
||||||
|
val isDm: Boolean,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
fun RoomPreviewInfo.toInviteData(): InviteData {
|
||||||
|
return InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = name ?: roomId.value,
|
||||||
|
isDm = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun RoomInfo.toInviteData(): InviteData {
|
||||||
|
return InviteData(
|
||||||
|
roomId = id,
|
||||||
|
roomName = name ?: id.value,
|
||||||
|
isDm = isDm,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
|
||||||
|
interface AcceptDeclineInviteEvents {
|
||||||
|
data class AcceptInvite(val invite: InviteData) : AcceptDeclineInviteEvents
|
||||||
|
data class DeclineInvite(val invite: InviteData, val blockUser: Boolean, val shouldConfirm: Boolean) : AcceptDeclineInviteEvents
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
@ -5,12 +5,12 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
|
open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDeclineInviteState> {
|
||||||
override val values: Sequence<AcceptDeclineInviteState>
|
override val values: Sequence<AcceptDeclineInviteState>
|
||||||
|
|
@ -18,27 +18,21 @@ open class AcceptDeclineInviteStateProvider : PreviewParameterProvider<AcceptDec
|
||||||
anAcceptDeclineInviteState(),
|
anAcceptDeclineInviteState(),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
declineAction = ConfirmingDeclineInvite(
|
declineAction = ConfirmingDeclineInvite(
|
||||||
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
|
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
||||||
blockUser = false,
|
blockUser = false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
declineAction = ConfirmingDeclineInvite(
|
declineAction = ConfirmingDeclineInvite(
|
||||||
InviteData(roomId = RoomId("!room:matrix.org"), isDm = false, roomName = "Some room", senderId = UserId("@alice:matrix.org")),
|
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice"),
|
||||||
blockUser = false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
anAcceptDeclineInviteState(
|
|
||||||
declineAction = ConfirmingDeclineInvite(
|
|
||||||
InviteData(roomId = RoomId("!room:matrix.org"), isDm = true, roomName = "Alice", senderId = UserId("@alice:matrix.org")),
|
|
||||||
blockUser = true,
|
blockUser = true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
acceptAction = AsyncAction.Failure(Throwable("Whoops")),
|
acceptAction = AsyncAction.Failure(Throwable("Error while accepting invite")),
|
||||||
),
|
),
|
||||||
anAcceptDeclineInviteState(
|
anAcceptDeclineInviteState(
|
||||||
declineAction = AsyncAction.Failure(Throwable("Whoops")),
|
declineAction = AsyncAction.Failure(Throwable("Error while declining invite")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -15,8 +15,8 @@ interface AcceptDeclineInviteView {
|
||||||
@Composable
|
@Composable
|
||||||
fun Render(
|
fun Render(
|
||||||
state: AcceptDeclineInviteState,
|
state: AcceptDeclineInviteState,
|
||||||
onAcceptInvite: (RoomId) -> Unit,
|
onAcceptInviteSuccess: (RoomId) -> Unit,
|
||||||
onDeclineInvite: (RoomId) -> Unit,
|
onDeclineInviteSuccess: (RoomId) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,11 +5,9 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
package io.element.android.features.invite.api.acceptdecline
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
data class ConfirmingDeclineInvite(
|
data class ConfirmingDeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : AsyncAction.Confirming
|
||||||
val inviteData: InviteData,
|
|
||||||
val blockUser: Boolean,
|
|
||||||
) : AsyncAction.Confirming
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.api.declineandblock
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
|
||||||
|
interface DeclineInviteAndBlockEntryPoint : FeatureEntryPoint {
|
||||||
|
fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node
|
||||||
|
}
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
|
||||||
|
|
||||||
interface AcceptDeclineInviteEvents {
|
|
||||||
data class AcceptInvite(val invite: InviteData?) : AcceptDeclineInviteEvents
|
|
||||||
data class DeclineInvite(val invite: InviteData?, val blockUser: Boolean = false) : AcceptDeclineInviteEvents
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invite.api.response
|
|
||||||
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
|
|
||||||
data class InviteData(
|
|
||||||
val senderId: UserId,
|
|
||||||
val roomId: RoomId,
|
|
||||||
val roomName: String,
|
|
||||||
val isDm: Boolean,
|
|
||||||
)
|
|
||||||
|
|
@ -14,6 +14,11 @@ plugins {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.invite.impl"
|
namespace = "io.element.android.features.invite.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAnvil()
|
setupAnvil()
|
||||||
|
|
@ -36,9 +41,12 @@ dependencies {
|
||||||
testImplementation(libs.molecule.runtime)
|
testImplementation(libs.molecule.runtime)
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
testImplementation(projects.features.invite.test)
|
testImplementation(projects.features.invite.test)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
testImplementation(projects.libraries.push.test)
|
testImplementation(projects.libraries.push.test)
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface AcceptInvite {
|
||||||
|
suspend operator fun invoke(roomId: RoomId): Result<RoomId>
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultAcceptInvite @Inject constructor(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val joinRoom: JoinRoom,
|
||||||
|
private val notificationCleaner: NotificationCleaner,
|
||||||
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
) : AcceptInvite {
|
||||||
|
override suspend fun invoke(roomId: RoomId): Result<RoomId> {
|
||||||
|
return joinRoom(
|
||||||
|
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
||||||
|
serverNames = emptyList(),
|
||||||
|
trigger = JoinedRoom.Trigger.Invite,
|
||||||
|
).onSuccess {
|
||||||
|
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
||||||
|
seenInvitesStore.markAsUnSeen(roomId)
|
||||||
|
}.map { roomId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface DeclineInvite {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
roomId: RoomId,
|
||||||
|
blockUser: Boolean,
|
||||||
|
reportRoom: Boolean,
|
||||||
|
reportReason: String?
|
||||||
|
): Result<RoomId>
|
||||||
|
|
||||||
|
sealed class Exception : kotlin.Exception() {
|
||||||
|
data object RoomNotFound : Exception()
|
||||||
|
data object DeclineInviteFailed : Exception()
|
||||||
|
data object ReportRoomFailed : Exception()
|
||||||
|
data object BlockUserFailed : Exception()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultDeclineInvite @Inject constructor(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
private val notificationCleaner: NotificationCleaner,
|
||||||
|
private val seenInvitesStore: SeenInvitesStore,
|
||||||
|
) : DeclineInvite {
|
||||||
|
override suspend fun invoke(
|
||||||
|
roomId: RoomId,
|
||||||
|
blockUser: Boolean,
|
||||||
|
reportRoom: Boolean,
|
||||||
|
reportReason: String?
|
||||||
|
): Result<RoomId> {
|
||||||
|
val room = client.getRoom(roomId) ?: return Result.failure(DeclineInvite.Exception.RoomNotFound)
|
||||||
|
room.use {
|
||||||
|
room.leave()
|
||||||
|
.onFailure { return Result.failure(DeclineInvite.Exception.DeclineInviteFailed) }
|
||||||
|
.onSuccess {
|
||||||
|
notificationCleaner.clearMembershipNotificationForRoom(
|
||||||
|
sessionId = client.sessionId,
|
||||||
|
roomId = roomId
|
||||||
|
)
|
||||||
|
seenInvitesStore.markAsUnSeen(roomId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blockUser) {
|
||||||
|
val userIdToBlock = room.info().inviter?.userId
|
||||||
|
if (userIdToBlock != null) {
|
||||||
|
client
|
||||||
|
.ignoreUser(userIdToBlock)
|
||||||
|
.onFailure { return Result.failure(DeclineInvite.Exception.BlockUserFailed) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reportRoom) {
|
||||||
|
room
|
||||||
|
.reportRoom(reportReason)
|
||||||
|
.onFailure { return Result.failure(DeclineInvite.Exception.ReportRoomFailed) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
|
||||||
|
import io.element.android.features.invite.impl.AcceptInvite
|
||||||
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
|
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.core.RoomId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class AcceptDeclineInvitePresenter @Inject constructor(
|
||||||
|
private val acceptInvite: AcceptInvite,
|
||||||
|
private val declineInvite: DeclineInvite,
|
||||||
|
) : Presenter<AcceptDeclineInviteState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): AcceptDeclineInviteState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val acceptedAction: MutableState<AsyncAction<RoomId>> =
|
||||||
|
remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
val declinedAction: MutableState<AsyncAction<RoomId>> =
|
||||||
|
remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
fun handleEvents(event: AcceptDeclineInviteEvents) {
|
||||||
|
when (event) {
|
||||||
|
is AcceptDeclineInviteEvents.AcceptInvite -> {
|
||||||
|
localCoroutineScope.acceptInvite(event.invite.roomId, acceptedAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
||||||
|
val inviteData = event.invite
|
||||||
|
if (event.shouldConfirm) {
|
||||||
|
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
|
||||||
|
} else {
|
||||||
|
localCoroutineScope.declineInvite(
|
||||||
|
inviteData = inviteData,
|
||||||
|
blockUser = event.blockUser,
|
||||||
|
declinedAction = declinedAction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
||||||
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
|
||||||
|
acceptedAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
|
||||||
|
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
|
||||||
|
declinedAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AcceptDeclineInviteState(
|
||||||
|
acceptAction = acceptedAction.value,
|
||||||
|
declineAction = declinedAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.acceptInvite(
|
||||||
|
roomId: RoomId,
|
||||||
|
acceptedAction: MutableState<AsyncAction<RoomId>>,
|
||||||
|
) = launch {
|
||||||
|
acceptedAction.runUpdatingState {
|
||||||
|
acceptInvite(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.declineInvite(
|
||||||
|
inviteData: InviteData,
|
||||||
|
blockUser: Boolean,
|
||||||
|
declinedAction: MutableState<AsyncAction<RoomId>>,
|
||||||
|
) = launch {
|
||||||
|
declinedAction.runUpdatingState {
|
||||||
|
declineInvite(
|
||||||
|
roomId = inviteData.roomId,
|
||||||
|
blockUser = blockUser,
|
||||||
|
reportRoom = false,
|
||||||
|
reportReason = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,17 +5,18 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteStateProvider
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
|
||||||
import io.element.android.features.invite.impl.R
|
import io.element.android.features.invite.impl.R
|
||||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
|
||||||
|
|
@ -27,21 +28,21 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
@Composable
|
@Composable
|
||||||
fun AcceptDeclineInviteView(
|
fun AcceptDeclineInviteView(
|
||||||
state: AcceptDeclineInviteState,
|
state: AcceptDeclineInviteState,
|
||||||
onAcceptInvite: (RoomId) -> Unit,
|
onAcceptInviteSuccess: (RoomId) -> Unit,
|
||||||
onDeclineInvite: (RoomId) -> Unit,
|
onDeclineInviteSuccess: (RoomId) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.acceptAction,
|
async = state.acceptAction,
|
||||||
onSuccess = onAcceptInvite,
|
onSuccess = onAcceptInviteSuccess,
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
|
state.eventSink(InternalAcceptDeclineInviteEvents.DismissAcceptError)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
AsyncActionView(
|
AsyncActionView(
|
||||||
async = state.declineAction,
|
async = state.declineAction,
|
||||||
onSuccess = onDeclineInvite,
|
onSuccess = onDeclineInviteSuccess,
|
||||||
onErrorDismiss = {
|
onErrorDismiss = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
state.eventSink(InternalAcceptDeclineInviteEvents.DismissDeclineError)
|
||||||
},
|
},
|
||||||
|
|
@ -52,7 +53,13 @@ fun AcceptDeclineInviteView(
|
||||||
invite = confirming.inviteData,
|
invite = confirming.inviteData,
|
||||||
blockUser = confirming.blockUser,
|
blockUser = confirming.blockUser,
|
||||||
onConfirmClick = {
|
onConfirmClick = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite)
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(
|
||||||
|
confirming.inviteData,
|
||||||
|
blockUser = confirming.blockUser,
|
||||||
|
shouldConfirm = false
|
||||||
|
)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onDismissClick = {
|
onDismissClick = {
|
||||||
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
state.eventSink(InternalAcceptDeclineInviteEvents.CancelDeclineInvite)
|
||||||
|
|
@ -72,30 +79,21 @@ private fun DeclineConfirmationDialog(
|
||||||
onDismissClick: () -> Unit,
|
onDismissClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val senderId = invite.senderId.value
|
|
||||||
val content = when {
|
|
||||||
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_message, senderId)
|
|
||||||
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_message, invite.roomName)
|
|
||||||
else -> stringResource(R.string.screen_invites_decline_chat_message, invite.roomName)
|
|
||||||
}
|
|
||||||
val title = when {
|
|
||||||
blockUser -> stringResource(R.string.screen_join_room_decline_and_block_alert_title)
|
|
||||||
invite.isDm -> stringResource(R.string.screen_invites_decline_direct_chat_title)
|
|
||||||
else -> stringResource(R.string.screen_invites_decline_chat_title)
|
|
||||||
}
|
|
||||||
val submitText = if (blockUser) {
|
|
||||||
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
|
|
||||||
} else {
|
|
||||||
stringResource(CommonStrings.action_decline)
|
|
||||||
}
|
|
||||||
ConfirmationDialog(
|
ConfirmationDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
content = content,
|
content = stringResource(R.string.screen_invites_decline_chat_message, invite.roomName),
|
||||||
title = title,
|
title = if (blockUser) {
|
||||||
submitText = submitText,
|
stringResource(R.string.screen_join_room_decline_and_block_alert_title)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.screen_invites_decline_chat_title)
|
||||||
|
},
|
||||||
|
submitText = if (blockUser) {
|
||||||
|
stringResource(R.string.screen_join_room_decline_and_block_alert_confirmation)
|
||||||
|
} else {
|
||||||
|
stringResource(CommonStrings.action_decline)
|
||||||
|
},
|
||||||
cancelText = stringResource(CommonStrings.action_cancel),
|
cancelText = stringResource(CommonStrings.action_cancel),
|
||||||
onSubmitClick = onConfirmClick,
|
onSubmitClick = onConfirmClick,
|
||||||
destructiveSubmit = blockUser,
|
|
||||||
onDismiss = onDismissClick,
|
onDismiss = onDismissClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -106,7 +104,7 @@ internal fun AcceptDeclineInviteViewPreview(@PreviewParameter(AcceptDeclineInvit
|
||||||
ElementPreview {
|
ElementPreview {
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
state = state,
|
state = state,
|
||||||
onAcceptInvite = {},
|
onAcceptInviteSuccess = {},
|
||||||
onDeclineInvite = {},
|
onDeclineInviteSuccess = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,13 +5,13 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.squareup.anvil.annotations.ContributesBinding
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -21,14 +21,14 @@ class DefaultAcceptDeclineInviteView @Inject constructor() : AcceptDeclineInvite
|
||||||
@Composable
|
@Composable
|
||||||
override fun Render(
|
override fun Render(
|
||||||
state: AcceptDeclineInviteState,
|
state: AcceptDeclineInviteState,
|
||||||
onAcceptInvite: (RoomId) -> Unit,
|
onAcceptInviteSuccess: (RoomId) -> Unit,
|
||||||
onDeclineInvite: (RoomId) -> Unit,
|
onDeclineInviteSuccess: (RoomId) -> Unit,
|
||||||
modifier: Modifier,
|
modifier: Modifier,
|
||||||
) {
|
) {
|
||||||
AcceptDeclineInviteView(
|
AcceptDeclineInviteView(
|
||||||
state = state,
|
state = state,
|
||||||
onAcceptInvite = onAcceptInvite,
|
onAcceptInviteSuccess = onAcceptInviteSuccess,
|
||||||
onDeclineInvite = onDeclineInvite,
|
onDeclineInviteSuccess = onDeclineInviteSuccess,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,12 +5,11 @@
|
||||||
* Please see LICENSE files in the repository root for full details.
|
* Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
|
|
||||||
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
sealed interface InternalAcceptDeclineInviteEvents : AcceptDeclineInviteEvents {
|
||||||
data object ConfirmDeclineInvite : InternalAcceptDeclineInviteEvents
|
|
||||||
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
data object CancelDeclineInvite : InternalAcceptDeclineInviteEvents
|
||||||
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
data object DismissAcceptError : InternalAcceptDeclineInviteEvents
|
||||||
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
data object DismissDeclineError : InternalAcceptDeclineInviteEvents
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
sealed interface DeclineAndBlockEvents {
|
||||||
|
data class UpdateReportReason(val reason: String) : DeclineAndBlockEvents
|
||||||
|
data object ToggleReportRoom : DeclineAndBlockEvents
|
||||||
|
data object ToggleBlockUser : DeclineAndBlockEvents
|
||||||
|
data object Decline : DeclineAndBlockEvents
|
||||||
|
data object ClearDeclineAction : DeclineAndBlockEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class DeclineAndBlockNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: DeclineAndBlockPresenter.Factory,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
data class Inputs(val inviteData: InviteData) : NodeInputs
|
||||||
|
|
||||||
|
private val inviteData = inputs<Inputs>().inviteData
|
||||||
|
private val presenter = presenterFactory.create(inviteData)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
DeclineAndBlockView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = ::navigateUp,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarMessage
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class DeclineAndBlockPresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val inviteData: InviteData,
|
||||||
|
private val declineInvite: DeclineInvite,
|
||||||
|
private val snackbarDispatcher: SnackbarDispatcher,
|
||||||
|
) : Presenter<DeclineAndBlockState> {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(inviteData: InviteData): DeclineAndBlockPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): DeclineAndBlockState {
|
||||||
|
var reportReason by rememberSaveable { mutableStateOf("") }
|
||||||
|
var blockUser by rememberSaveable { mutableStateOf(true) }
|
||||||
|
var reportRoom by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val declineAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvents(event: DeclineAndBlockEvents) {
|
||||||
|
when (event) {
|
||||||
|
DeclineAndBlockEvents.ClearDeclineAction -> declineAction.value = AsyncAction.Uninitialized
|
||||||
|
DeclineAndBlockEvents.Decline -> coroutineScope.decline(reportReason, blockUser, reportRoom, declineAction)
|
||||||
|
DeclineAndBlockEvents.ToggleBlockUser -> blockUser = !blockUser
|
||||||
|
DeclineAndBlockEvents.ToggleReportRoom -> reportRoom = !reportRoom
|
||||||
|
is DeclineAndBlockEvents.UpdateReportReason -> reportReason = event.reason
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DeclineAndBlockState(
|
||||||
|
reportRoom = reportRoom,
|
||||||
|
reportReason = reportReason,
|
||||||
|
blockUser = blockUser,
|
||||||
|
declineAction = declineAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.decline(
|
||||||
|
reason: String,
|
||||||
|
blockUser: Boolean,
|
||||||
|
reportRoom: Boolean,
|
||||||
|
action: MutableState<AsyncAction<Unit>>
|
||||||
|
) = launch {
|
||||||
|
action.value = AsyncAction.Loading
|
||||||
|
declineInvite(
|
||||||
|
roomId = inviteData.roomId,
|
||||||
|
blockUser = blockUser,
|
||||||
|
reportRoom = reportRoom,
|
||||||
|
reportReason = reason
|
||||||
|
).onSuccess {
|
||||||
|
action.value = AsyncAction.Success(Unit)
|
||||||
|
}.onFailure { error ->
|
||||||
|
if (error is DeclineInvite.Exception.DeclineInviteFailed) {
|
||||||
|
action.value = AsyncAction.Failure(error)
|
||||||
|
} else {
|
||||||
|
action.value = AsyncAction.Uninitialized
|
||||||
|
snackbarDispatcher.post(SnackbarMessage(CommonStrings.error_unknown))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
data class DeclineAndBlockState(
|
||||||
|
val reportRoom: Boolean,
|
||||||
|
val reportReason: String,
|
||||||
|
val blockUser: Boolean,
|
||||||
|
val declineAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (DeclineAndBlockEvents) -> Unit
|
||||||
|
) {
|
||||||
|
val canDecline = blockUser || reportRoom && reportReason.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
open class DeclineAndBlockStateProvider : PreviewParameterProvider<DeclineAndBlockState> {
|
||||||
|
override val values: Sequence<DeclineAndBlockState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aDeclineAndBlockState(),
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
reportRoom = true,
|
||||||
|
reportReason = "Inappropriate content",
|
||||||
|
),
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
blockUser = true,
|
||||||
|
),
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
declineAction = AsyncAction.Loading,
|
||||||
|
),
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
declineAction = AsyncAction.Failure(Exception("Failed to decline")),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aDeclineAndBlockState(
|
||||||
|
reportRoom: Boolean = false,
|
||||||
|
reportReason: String = "",
|
||||||
|
blockUser: Boolean = false,
|
||||||
|
declineAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (DeclineAndBlockEvents) -> Unit = {},
|
||||||
|
) = DeclineAndBlockState(
|
||||||
|
reportRoom = reportRoom,
|
||||||
|
reportReason = reportReason,
|
||||||
|
blockUser = blockUser,
|
||||||
|
declineAction = declineAction,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,159 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.invite.impl.R
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
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.ListItem
|
||||||
|
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.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DeclineAndBlockView(
|
||||||
|
state: DeclineAndBlockState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val isDeclining = state.declineAction is AsyncAction.Loading
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.declineAction,
|
||||||
|
onSuccess = { onBackClick() },
|
||||||
|
errorMessage = { stringResource(CommonStrings.error_unknown) },
|
||||||
|
onRetry = { state.eventSink(DeclineAndBlockEvents.Decline) },
|
||||||
|
onErrorDismiss = { state.eventSink(DeclineAndBlockEvents.ClearDeclineAction) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.screen_decline_and_block_title),
|
||||||
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.imePadding()
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_title))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = stringResource(R.string.screen_decline_and_block_block_user_option_description))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Switch(checked = state.blockUser)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(CommonStrings.action_report_room))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(text = stringResource(R.string.screen_decline_and_block_report_user_option_description))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Switch(checked = state.reportRoom)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (state.reportRoom) {
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
TextField(
|
||||||
|
value = state.reportReason,
|
||||||
|
onValueChange = { state.eventSink(DeclineAndBlockEvents.UpdateReportReason(it)) },
|
||||||
|
placeholder = stringResource(R.string.screen_decline_and_block_report_user_reason_placeholder),
|
||||||
|
minLines = 3,
|
||||||
|
enabled = !isDeclining,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.heightIn(min = 90.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_decline),
|
||||||
|
destructive = true,
|
||||||
|
showProgress = isDeclining,
|
||||||
|
enabled = !isDeclining && state.canDecline,
|
||||||
|
onClick = {
|
||||||
|
focusManager.clearFocus(force = true)
|
||||||
|
state.eventSink(DeclineAndBlockEvents.Decline)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun DeclineAndBlockViewPreview(
|
||||||
|
@PreviewParameter(DeclineAndBlockStateProvider::class) state: DeclineAndBlockState
|
||||||
|
) = ElementPreview {
|
||||||
|
DeclineAndBlockView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultDeclineAndBlockEntryPoint @Inject constructor() : DeclineInviteAndBlockEntryPoint {
|
||||||
|
override fun createNode(parentNode: Node, buildContext: BuildContext, inviteData: InviteData): Node {
|
||||||
|
val inputs = DeclineAndBlockNode.Inputs(inviteData)
|
||||||
|
return parentNode.createNode<DeclineAndBlockNode>(buildContext, plugins = listOf(inputs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,9 +12,9 @@ import dagger.Binds
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
|
import io.element.android.features.invite.impl.SeenInvitesStoreFactory
|
||||||
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
import io.element.android.features.invite.impl.acceptdecline.AcceptDeclineInvitePresenter
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.MutableState
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
|
||||||
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.architecture.Presenter
|
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
|
||||||
import io.element.android.libraries.architecture.runUpdatingState
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
|
||||||
import io.element.android.libraries.matrix.api.room.join.JoinRoom
|
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class AcceptDeclineInvitePresenter @Inject constructor(
|
|
||||||
private val client: MatrixClient,
|
|
||||||
private val joinRoom: JoinRoom,
|
|
||||||
private val notificationCleaner: NotificationCleaner,
|
|
||||||
private val seenInvitesStore: SeenInvitesStore,
|
|
||||||
) : Presenter<AcceptDeclineInviteState> {
|
|
||||||
@Composable
|
|
||||||
override fun present(): AcceptDeclineInviteState {
|
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
val acceptedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
|
||||||
val declinedAction: MutableState<AsyncAction<RoomId>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
|
||||||
|
|
||||||
fun handleEvents(event: AcceptDeclineInviteEvents) {
|
|
||||||
when (event) {
|
|
||||||
is AcceptDeclineInviteEvents.AcceptInvite -> {
|
|
||||||
val inviteData = event.invite
|
|
||||||
if (inviteData == null) {
|
|
||||||
acceptedAction.value = AsyncAction.Failure(InvalidDataException())
|
|
||||||
} else {
|
|
||||||
localCoroutineScope.acceptInvite(inviteData.roomId, acceptedAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is AcceptDeclineInviteEvents.DeclineInvite -> {
|
|
||||||
val inviteData = event.invite
|
|
||||||
if (inviteData == null) {
|
|
||||||
declinedAction.value = AsyncAction.Failure(InvalidDataException())
|
|
||||||
} else {
|
|
||||||
declinedAction.value = ConfirmingDeclineInvite(inviteData, event.blockUser)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite -> {
|
|
||||||
when (val declinedActionValue = declinedAction.value) {
|
|
||||||
is ConfirmingDeclineInvite -> {
|
|
||||||
localCoroutineScope.declineInvite(
|
|
||||||
inviteData = declinedActionValue.inviteData,
|
|
||||||
declinedAction = declinedAction,
|
|
||||||
blockUser = declinedActionValue.blockUser,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.CancelDeclineInvite -> {
|
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.DismissAcceptError -> {
|
|
||||||
acceptedAction.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
|
|
||||||
is InternalAcceptDeclineInviteEvents.DismissDeclineError -> {
|
|
||||||
declinedAction.value = AsyncAction.Uninitialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return AcceptDeclineInviteState(
|
|
||||||
acceptAction = acceptedAction.value,
|
|
||||||
declineAction = declinedAction.value,
|
|
||||||
eventSink = ::handleEvents
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.acceptInvite(
|
|
||||||
roomId: RoomId,
|
|
||||||
acceptedAction: MutableState<AsyncAction<RoomId>>,
|
|
||||||
) = launch {
|
|
||||||
acceptedAction.runUpdatingState {
|
|
||||||
joinRoom(
|
|
||||||
roomIdOrAlias = roomId.toRoomIdOrAlias(),
|
|
||||||
serverNames = emptyList(),
|
|
||||||
trigger = JoinedRoom.Trigger.Invite,
|
|
||||||
)
|
|
||||||
.onSuccess {
|
|
||||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, roomId)
|
|
||||||
seenInvitesStore.markAsUnSeen(roomId)
|
|
||||||
}
|
|
||||||
.map { roomId }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CoroutineScope.declineInvite(
|
|
||||||
inviteData: InviteData,
|
|
||||||
blockUser: Boolean,
|
|
||||||
declinedAction: MutableState<AsyncAction<RoomId>>,
|
|
||||||
) = launch {
|
|
||||||
suspend {
|
|
||||||
client.getRoom(inviteData.roomId)?.use {
|
|
||||||
it.leave().getOrThrow()
|
|
||||||
}
|
|
||||||
if (blockUser) {
|
|
||||||
client.ignoreUser(inviteData.senderId).getOrThrow()
|
|
||||||
}
|
|
||||||
notificationCleaner.clearMembershipNotificationForRoom(client.sessionId, inviteData.roomId)
|
|
||||||
seenInvitesStore.markAsUnSeen(inviteData.roomId)
|
|
||||||
inviteData.roomId
|
|
||||||
}.runCatchingUpdatingState(declinedAction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
|
||||||
|
|
||||||
class InvalidDataException : Exception()
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Od tohoto uživatele neuvidíte žádné zprávy ani pozvánky do místnosti"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Zablokovat uživatele"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Nahlaste tuto místnost svému poskytovateli účtu."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Popište důvod nahlášení…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Odmítnout a zablokovat"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Opravdu chcete odmítnout pozvánku do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
|
<string name="screen_invites_decline_chat_title">"Odmítnout pozvání"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Opravdu chcete odmítnout tuto soukromou konverzaci s %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Fyddwch chi ddim yn gweld unrhyw negeseuon neu wahoddiadau ystafell gan y defnyddiwr hwn"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Rhwystro defnyddiwr"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Adrodd am yr ystafell hon i ddarparwr eich cyfrif."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Disgrifiwch y rheswm dros adrodd…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Gwrthod a rhwystro"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y gwahoddiad i ymuno â %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
|
<string name="screen_invites_decline_chat_title">"Gwrthod y gwahoddiad"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Ydych chi\'n siŵr eich bod am wrthod y sgwrs breifat hon gyda %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Sie werden keine Nachrichten oder Chatroomeinladungen von diesem Benutzer sehen."</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Benutzer blockieren"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Melden Sie diesen Raum Ihrem Kontoanbieter."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beschreiben Sie den Grund für die Meldung…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Ablehnen und blockieren"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
<string name="screen_invites_decline_chat_message">"Möchtest du die Einladung zum Betreten von %1$s wirklich ablehnen?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
|
<string name="screen_invites_decline_chat_title">"Einladung ablehnen"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Bist du sicher, dass du diese Direktnachricht von %1$s ablehnen möchtest?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Δε θα δείτε μηνύματα ή προσκλήσεις δωματίου από αυτόν τον χρήστη"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Αποκλεισμός χρήστη"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Αναφέρετε αυτό το δωμάτιο στον πάροχο του λογαριασμού σας."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Περιγράψτε τον λόγο αναφοράς…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Απόρριψη και αποκλεισμός"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;"</string>
|
<string name="screen_invites_decline_chat_message">"Σίγουρα θες να απορρίψεις την πρόσκληση συμμετοχής στο %1$s;"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Απόρριψη πρόσκλησης"</string>
|
<string name="screen_invites_decline_chat_title">"Απόρριψη πρόσκλησης"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Σίγουρα θες να απορρίψεις την ιδιωτική συνομιλία με τον χρήστη %1$s;"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Sa ei näe enam selle kasutaja saadetud sõnumeid ja jututubade kutseid"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Blokeeri kasutaja"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Teata sellest jututoast oma teenusepakkujale."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Kirjelda teatamise põhjust…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Keeldu ja blokeeri"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Kas sa oled kindel, et soovid keelduda liitumiskutsest: %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
|
<string name="screen_invites_decline_chat_title">"Lükka kutse tagasi"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Kas sa oled kindel, et soovid keelduda privaatsest vestlusest kasutajaga %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Et tule näkemään viestejä tai kutsuja tältä käyttäjältä"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Estä käyttäjä"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Ilmoita tästä huoneesta palveluntarjoajallesi."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Kerro syy ilmoittamiseen…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Hylkää ja estä"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"</string>
|
<string name="screen_invites_decline_chat_message">"Haluatko varmasti hylätä kutsun liittyä %1$s -huoneeseen?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Hylkää kutsu"</string>
|
<string name="screen_invites_decline_chat_title">"Hylkää kutsu"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Haluatko varmasti hylätä kutsun yksityiseen keskusteluun käyttäjän %1$s kanssa?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Vous ne verrez aucun messages ou invitation à un salon de la part de cet utilisateur"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Bloquer l’utilisateur"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Signalez ce salon à votre fournisseur de compte."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Décrivez la raison du signalement…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Refuser et bloquer"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?"</string>
|
<string name="screen_invites_decline_chat_message">"Êtes-vous sûr de vouloir décliner l’invitation à rejoindre %1$s ?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</string>
|
<string name="screen_invites_decline_chat_title">"Refuser l’invitation"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Êtes-vous sûr de vouloir refuser cette discussion privée avec %1$s ?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Ettől a felhasználótól nem fog többé üzeneteket vagy meghívásokat látni."</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Felhasználó letiltása"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"A szoba jelentése a fiókszolgáltatójának."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Írja le a jelentés okát…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Elutasítás és blokkolás"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Biztos, hogy elutasítja a meghívást, hogy csatlakozzon ehhez: %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
|
<string name="screen_invites_decline_chat_title">"Meghívás elutasítása"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Biztos, hogy elutasítja ezt a privát csevegést vele: %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Du vil ikke se noen meldinger eller rominvitasjoner fra denne brukeren"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Blokker bruker"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Rapporter dette rommet til din kontoleverandør."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beskriv årsaken for å rapportere…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Avslå og blokker"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Er du sikker på at du vil takke nei til invitasjonen til å bli med i %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
|
<string name="screen_invites_decline_chat_title">"Avvis invitasjon"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Er du sikker på at du vil avslå denne private chatten med %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Nie zobaczysz żadnych wiadomości ani zaproszeń od tego użytkownika"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Zablokuj użytkownika"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Zgłoś pokój dostawcy swojego konta."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Opisz powód zgłoszenia…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Odrzuć i zablokuj"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Czy na pewno chcesz odrzucić zaproszenie dołączenia do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
<string name="screen_invites_decline_chat_title">"Odrzuć zaproszenie"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Czy na pewno chcesz odrzucić rozmowę prywatną z %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Заблокировать пользователя"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Опишите причину жалобы…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Отклонить и заблокировать"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Вы уверены, что хотите отклонить приглашение в %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
|
<string name="screen_invites_decline_chat_title">"Отклонить приглашение"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Вы уверены, что хотите отказаться от личного общения с %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Od tohto používateľa sa vám nezobrazia žiadne správy ani pozvánky do miestnosti"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Zablokovať používateľa"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Nahlásiť túto miestnosť poskytovateľovi účtu."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Opíšte dôvod nahlásenia…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Odmietnuť a zablokovať"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Naozaj chcete odmietnuť pozvánku na pripojenie do %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
|
<string name="screen_invites_decline_chat_title">"Odmietnuť pozvanie"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Naozaj chcete odmietnuť túto súkromnú konverzáciu s %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Du kommer inte att se några meddelanden eller rumsinbjudningar från den här användaren"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Blockera användare"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Rapportera det här rummet till din kontoleverantör."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Beskriv skälet för anmälan …"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Avvisa och blockera"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Är du säker på att du vill tacka nej till inbjudan att gå med%1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string>
|
<string name="screen_invites_decline_chat_title">"Avböj inbjudan"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Är du säker på att du vill avböja denna privata chatt med %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"Ви не бачитимете повідомлень або запрошень у кімнату від цього користувача"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Заблокувати користувача"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Поскаржитися на цю кімнату постачальнику облікового запису."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Опишіть причину скарги…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Відхилити та заблокувати"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Ви впевнені, що хочете відхилити запрошення приєднатися до %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
|
<string name="screen_invites_decline_chat_title">"Відхилити запрошення"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Ви дійсно хочете відмовитися від приватної бесіди з %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"您將不會看到來自此使用者的任何訊息或聊天室邀請"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"封鎖使用者"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"向您的帳號提供者回報此聊天室。"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"拒絕並封鎖"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
|
<string name="screen_invites_decline_chat_message">"您確定您想要拒絕加入 %1$s 的邀請嗎?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
|
<string name="screen_invites_decline_chat_title">"拒絕邀請"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"您確定您要拒絕此與 %1$s 的私人聊天嗎?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"您確定您要拒絕此與 %1$s 的私人聊天嗎?"</string>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_decline_and_block_block_user_option_description">"You will not see any messages or room invites from this user"</string>
|
||||||
|
<string name="screen_decline_and_block_block_user_option_title">"Block user"</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_option_description">"Report this room to your account provider."</string>
|
||||||
|
<string name="screen_decline_and_block_report_user_reason_placeholder">"Describe the reason to report…"</string>
|
||||||
|
<string name="screen_decline_and_block_title">"Decline and block"</string>
|
||||||
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
|
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
|
||||||
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
|
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
|
||||||
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>
|
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultAcceptInviteTest {
|
||||||
|
private val roomId = A_ROOM_ID
|
||||||
|
private val client = FakeMatrixClient()
|
||||||
|
private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId))
|
||||||
|
|
||||||
|
private val clearMembershipNotificationForRoomLambda =
|
||||||
|
lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
private val notificationCleaner =
|
||||||
|
FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `accept invite success scenario`() = runTest {
|
||||||
|
val joinRoomLambda =
|
||||||
|
lambdaRecorder<RoomIdOrAlias, List<String>, JoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
|
||||||
|
Result.success(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
val acceptInvite = DefaultAcceptInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
joinRoom = FakeJoinRoom(lambda = joinRoomLambda),
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = acceptInvite(roomId)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
|
||||||
|
assert(joinRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(roomId.toRoomIdOrAlias()), any(), any())
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(client.sessionId), value(roomId))
|
||||||
|
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `accept invite failure scenario`() = runTest {
|
||||||
|
val joinRoomLambda =
|
||||||
|
lambdaRecorder<RoomIdOrAlias, List<String>, JoinedRoom.Trigger, Result<Unit>> { _, _, _ ->
|
||||||
|
Result.failure(Throwable("Join room failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
val acceptInvite = DefaultAcceptInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
joinRoom = FakeJoinRoom(lambda = joinRoomLambda),
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
|
||||||
|
val result = acceptInvite(roomId)
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
|
||||||
|
assert(joinRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(roomId.toRoomIdOrAlias()), any(), any())
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda).isNeverCalled()
|
||||||
|
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,178 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.api.core.SessionId
|
||||||
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomInfo
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultDeclineInviteTest {
|
||||||
|
private val roomId = A_ROOM_ID
|
||||||
|
private val inviter = aRoomMember()
|
||||||
|
private val seenInvitesStore = InMemorySeenInvitesStore(initialRoomIds = setOf(roomId))
|
||||||
|
private val clearMembershipNotificationForRoomLambda =
|
||||||
|
lambdaRecorder<SessionId, RoomId, Unit> { _, _ -> }
|
||||||
|
private val notificationCleaner =
|
||||||
|
FakeNotificationCleaner(clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda)
|
||||||
|
|
||||||
|
private val successLeaveRoomLambda = lambdaRecorder<Result<Unit>> { -> Result.success(Unit) }
|
||||||
|
private val successIgnoreUserLambda =
|
||||||
|
lambdaRecorder<UserId, Result<Unit>> { _ -> Result.success(Unit) }
|
||||||
|
private val successReportRoomLambda =
|
||||||
|
lambdaRecorder<String?, Result<Unit>> { _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
private val failureLeaveRoomLambda =
|
||||||
|
lambdaRecorder<Result<Unit>> { -> Result.failure(Exception("Leave room error")) }
|
||||||
|
private val failureIgnoreUserLambda =
|
||||||
|
lambdaRecorder<UserId, Result<Unit>> { _ -> Result.failure(Exception("Ignore user error")) }
|
||||||
|
private val failureReportRoomLambda =
|
||||||
|
lambdaRecorder<String?, Result<Unit>> { _ -> Result.failure(Exception("Report room error")) }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decline invite, block=false, report=false, all success`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
|
||||||
|
val declineInvite = DefaultDeclineInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
|
||||||
|
val result =
|
||||||
|
declineInvite(roomId, blockUser = false, reportRoom = false, reportReason = null)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(client.sessionId), value(roomId))
|
||||||
|
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decline invite, block=true, report=true, all success`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda,
|
||||||
|
initialRoomInfo = aRoomInfo(inviter = inviter)
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val declineInvite = DefaultDeclineInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(client.sessionId), value(roomId))
|
||||||
|
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decline invite, block=true, report=true, decline invite failed`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = failureLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val declineInvite = DefaultDeclineInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
|
||||||
|
|
||||||
|
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.DeclineInviteFailed)
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda)
|
||||||
|
.isNeverCalled()
|
||||||
|
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decline invite, block=true, report=true, ignore user failed`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda,
|
||||||
|
initialRoomInfo = aRoomInfo(inviter = inviter)
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient(ignoreUserResult = failureIgnoreUserLambda).apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val declineInvite = DefaultDeclineInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
|
||||||
|
|
||||||
|
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.BlockUserFailed)
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda).isCalledOnce()
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decline invite, block=true, report=true, report room failed`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = failureReportRoomLambda,
|
||||||
|
initialRoomInfo = aRoomInfo(inviter = inviter)
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient(ignoreUserResult = successIgnoreUserLambda).apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val declineInvite = DefaultDeclineInvite(
|
||||||
|
client = client,
|
||||||
|
notificationCleaner = notificationCleaner,
|
||||||
|
seenInvitesStore = seenInvitesStore
|
||||||
|
)
|
||||||
|
val result = declineInvite(roomId, blockUser = true, reportRoom = true, reportReason = null)
|
||||||
|
|
||||||
|
assertThat(result.exceptionOrNull()).isEqualTo(DeclineInvite.Exception.ReportRoomFailed)
|
||||||
|
|
||||||
|
assert(clearMembershipNotificationForRoomLambda).isCalledOnce()
|
||||||
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.acceptdecline
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.ConfirmingDeclineInvite
|
||||||
|
import io.element.android.features.invite.impl.AcceptInvite
|
||||||
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
|
import io.element.android.features.invite.impl.fake.FakeAcceptInvite
|
||||||
|
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class AcceptDeclineInvitePresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite cancel flow`() = runTest {
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter()
|
||||||
|
presenter.test {
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
||||||
|
state.eventSink(
|
||||||
|
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite error flow`() = runTest {
|
||||||
|
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
|
||||||
|
Result.failure(DeclineInvite.Exception.DeclineInviteFailed)
|
||||||
|
}
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(
|
||||||
|
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(declineInviteFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID), value(false), value(false), value(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite success flow`() = runTest {
|
||||||
|
val declineInviteSuccess = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { roomId, _, _, _ -> Result.success(roomId) }
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, blockUser = false))
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = false)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(declineInviteSuccess)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID), value(false), value(false), value(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accepting invite error flow`() = runTest {
|
||||||
|
val acceptInviteFailure = lambdaRecorder<RoomId, Result<RoomId>> { roomId: RoomId ->
|
||||||
|
Result.failure(RuntimeException("Failed to accept invite"))
|
||||||
|
}
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
acceptInvite = FakeAcceptInvite(lambda = acceptInviteFailure),
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(
|
||||||
|
InternalAcceptDeclineInviteEvents.DismissAcceptError
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(acceptInviteFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - accepting invite success flow`() = runTest {
|
||||||
|
val acceptInviteSuccess = lambdaRecorder<RoomId, Result<RoomId>> { roomId: RoomId -> Result.success(roomId) }
|
||||||
|
val presenter = createAcceptDeclineInvitePresenter(
|
||||||
|
acceptInvite = FakeAcceptInvite(lambda = acceptInviteSuccess)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(
|
||||||
|
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
acceptInviteSuccess.assertions()
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anInviteData(
|
||||||
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
name: String = A_ROOM_NAME,
|
||||||
|
isDm: Boolean = false,
|
||||||
|
): InviteData {
|
||||||
|
return InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = name,
|
||||||
|
isDm = isDm,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createAcceptDeclineInvitePresenter(
|
||||||
|
acceptInvite: AcceptInvite = FakeAcceptInvite(),
|
||||||
|
declineInvite: DeclineInvite = FakeDeclineInvite(),
|
||||||
|
): AcceptDeclineInvitePresenter {
|
||||||
|
return AcceptDeclineInvitePresenter(
|
||||||
|
acceptInvite = acceptInvite,
|
||||||
|
declineInvite = declineInvite,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,175 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
|
import io.element.android.features.invite.impl.fake.FakeDeclineInvite
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DeclineAndBlockPresenterTest {
|
||||||
|
@get:Rule
|
||||||
|
val warmUpRule = WarmUpRule()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createDeclineAndBlockPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.blockUser).isTrue()
|
||||||
|
assertThat(state.reportRoom).isFalse()
|
||||||
|
assertThat(state.reportReason).isEmpty()
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
assertThat(state.canDecline).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - update form values`() = runTest {
|
||||||
|
val presenter = createDeclineAndBlockPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportRoom).isFalse()
|
||||||
|
assertThat(state.blockUser).isTrue()
|
||||||
|
assertThat(state.reportReason).isEmpty()
|
||||||
|
assertThat(state.canDecline).isTrue()
|
||||||
|
state.eventSink(DeclineAndBlockEvents.ToggleBlockUser)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportRoom).isFalse()
|
||||||
|
assertThat(state.blockUser).isFalse()
|
||||||
|
assertThat(state.reportReason).isEmpty()
|
||||||
|
assertThat(state.canDecline).isFalse()
|
||||||
|
state.eventSink(DeclineAndBlockEvents.ToggleReportRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportRoom).isTrue()
|
||||||
|
assertThat(state.blockUser).isFalse()
|
||||||
|
assertThat(state.reportReason).isEmpty()
|
||||||
|
assertThat(state.canDecline).isFalse()
|
||||||
|
state.eventSink(DeclineAndBlockEvents.UpdateReportReason("Spam"))
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportRoom).isTrue()
|
||||||
|
assertThat(state.blockUser).isFalse()
|
||||||
|
assertThat(state.reportReason).isEqualTo("Spam")
|
||||||
|
assertThat(state.canDecline).isTrue()
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite success flow`() = runTest {
|
||||||
|
val declineInviteSuccess = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { roomId, _, _, _ -> Result.success(roomId) }
|
||||||
|
val presenter = createDeclineAndBlockPresenter(
|
||||||
|
declineInvite = FakeDeclineInvite(lambda = declineInviteSuccess)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(DeclineAndBlockEvents.Decline)
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(declineInviteSuccess)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - declining invite error flow`() = runTest {
|
||||||
|
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
|
||||||
|
Result.failure(DeclineInvite.Exception.DeclineInviteFailed)
|
||||||
|
}
|
||||||
|
val presenter = createDeclineAndBlockPresenter(
|
||||||
|
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(DeclineAndBlockEvents.Decline)
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(DeclineAndBlockEvents.ClearDeclineAction)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(declineInviteFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - block user error flow`() = runTest {
|
||||||
|
val declineInviteFailure = lambdaRecorder<RoomId, Boolean, Boolean, String?, Result<RoomId>> { _, _, _, _ ->
|
||||||
|
Result.failure(DeclineInvite.Exception.BlockUserFailed)
|
||||||
|
}
|
||||||
|
val presenter = createDeclineAndBlockPresenter(
|
||||||
|
declineInvite = FakeDeclineInvite(lambda = declineInviteFailure)
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
state.eventSink(DeclineAndBlockEvents.Decline)
|
||||||
|
}
|
||||||
|
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
||||||
|
}
|
||||||
|
cancelAndConsumeRemainingEvents()
|
||||||
|
}
|
||||||
|
assert(declineInviteFailure)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(A_ROOM_ID), value(true), value(false), value(""))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anInviteData(
|
||||||
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
name: String = A_ROOM_NAME,
|
||||||
|
isDm: Boolean = false,
|
||||||
|
): InviteData {
|
||||||
|
return InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = name,
|
||||||
|
isDm = isDm,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDeclineAndBlockPresenter(
|
||||||
|
inviteData: InviteData = anInviteData(),
|
||||||
|
declineInvite: DeclineInvite = FakeDeclineInvite(),
|
||||||
|
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||||
|
): DeclineAndBlockPresenter {
|
||||||
|
return DeclineAndBlockPresenter(
|
||||||
|
inviteData = inviteData,
|
||||||
|
declineInvite = declineInvite,
|
||||||
|
snackbarDispatcher = snackbarDispatcher,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.declineandblock
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
|
import androidx.compose.ui.test.performTextInput
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.invite.impl.R
|
||||||
|
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 org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.rules.TestRule
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class DeclineAndBlockViewTest {
|
||||||
|
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on back invoke the expected callback`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
|
||||||
|
ensureCalledOnce {
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
onBackClick = it
|
||||||
|
)
|
||||||
|
rule.pressBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on decline when enabled emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
blockUser = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertSingle(DeclineAndBlockEvents.Decline)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on decline when disabled does not emit event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>(expectEvents = false)
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
blockUser = false,
|
||||||
|
reportRoom = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
|
eventsRecorder.assertEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on block option emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
blockUser = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_decline_and_block_block_user_option_title)
|
||||||
|
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleBlockUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on report room option emits the expected event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
reportRoom = true,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.clickOn(CommonStrings.action_report_room)
|
||||||
|
eventsRecorder.assertSingle(DeclineAndBlockEvents.ToggleReportRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `typing text in the reason field emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<DeclineAndBlockEvents>()
|
||||||
|
rule.setDeclineAndBlockView(
|
||||||
|
aDeclineAndBlockState(
|
||||||
|
reportRoom = true,
|
||||||
|
reportReason = "",
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
rule.onNodeWithText("").performTextInput("Spam!")
|
||||||
|
eventsRecorder.assertSingle(DeclineAndBlockEvents.UpdateReportReason("Spam!"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDeclineAndBlockView(
|
||||||
|
state: DeclineAndBlockState,
|
||||||
|
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||||
|
) {
|
||||||
|
setContent {
|
||||||
|
DeclineAndBlockView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.fake
|
||||||
|
|
||||||
|
import io.element.android.features.invite.impl.AcceptInvite
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeAcceptInvite(
|
||||||
|
private val lambda: (RoomId) -> Result<RoomId> = { lambdaError() },
|
||||||
|
) : AcceptInvite {
|
||||||
|
override suspend fun invoke(roomId: RoomId) = simulateLongTask {
|
||||||
|
lambda(roomId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.impl.fake
|
||||||
|
|
||||||
|
import io.element.android.features.invite.impl.DeclineInvite
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaError
|
||||||
|
import io.element.android.tests.testutils.simulateLongTask
|
||||||
|
|
||||||
|
class FakeDeclineInvite(
|
||||||
|
private val lambda: (RoomId, Boolean, Boolean, String?) -> Result<RoomId> = { _, _, _, _ -> lambdaError() },
|
||||||
|
) : DeclineInvite {
|
||||||
|
override suspend fun invoke(roomId: RoomId, blockUser: Boolean, reportRoom: Boolean, reportReason: String?): Result<RoomId> = simulateLongTask {
|
||||||
|
lambda(roomId, blockUser, reportRoom, reportReason)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,368 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.invite.impl.response
|
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
|
||||||
import io.element.android.features.invite.api.response.ConfirmingDeclineInvite
|
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
|
||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
|
||||||
import io.element.android.libraries.matrix.api.core.RoomIdOrAlias
|
|
||||||
import io.element.android.libraries.matrix.api.core.SessionId
|
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
|
||||||
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_3
|
|
||||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
|
||||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
|
||||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
|
||||||
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
|
||||||
import io.element.android.libraries.push.api.notifications.NotificationCleaner
|
|
||||||
import io.element.android.libraries.push.test.notifications.FakeNotificationCleaner
|
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
|
||||||
import io.element.android.tests.testutils.lambda.assert
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|
||||||
import io.element.android.tests.testutils.lambda.value
|
|
||||||
import io.element.android.tests.testutils.test
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.test.runTest
|
|
||||||
import org.junit.Rule
|
|
||||||
import org.junit.Test
|
|
||||||
|
|
||||||
class AcceptDeclineInvitePresenterTest {
|
|
||||||
@get:Rule
|
|
||||||
val warmUpRule = WarmUpRule()
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - initial state`() = runTest {
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter()
|
|
||||||
presenter.test {
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - declining invite cancel flow`() = runTest {
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.CancelDeclineInvite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - declining invite error flow`() = runTest {
|
|
||||||
val declineInviteFailure = lambdaRecorder { ->
|
|
||||||
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
|
||||||
}
|
|
||||||
val client = FakeMatrixClient().apply {
|
|
||||||
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure))
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
client = client,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Failure::class.java)
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.DismissDeclineError
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
|
||||||
}
|
|
||||||
cancelAndConsumeRemainingEvents()
|
|
||||||
}
|
|
||||||
assert(declineInviteFailure).isCalledOnce()
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - declining invite success flow`() = runTest {
|
|
||||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val fakeNotificationCleaner = FakeNotificationCleaner(
|
|
||||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
|
||||||
)
|
|
||||||
val declineInviteSuccess = lambdaRecorder { ->
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val client = FakeMatrixClient().apply {
|
|
||||||
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess))
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
client = client,
|
|
||||||
notificationCleaner = fakeNotificationCleaner,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, false))
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
|
||||||
}
|
|
||||||
cancelAndConsumeRemainingEvents()
|
|
||||||
}
|
|
||||||
declineInviteSuccess.assertions().isCalledOnce()
|
|
||||||
clearMembershipNotificationForRoomLambda.assertions()
|
|
||||||
.isCalledOnce()
|
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - declining invite with block success flow`() = runTest {
|
|
||||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val fakeNotificationCleaner = FakeNotificationCleaner(
|
|
||||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
|
||||||
)
|
|
||||||
val declineInviteSuccess = lambdaRecorder { -> Result.success(Unit) }
|
|
||||||
val ignoreUserSuccess = lambdaRecorder { _: UserId -> Result.success(Unit) }
|
|
||||||
val client = FakeMatrixClient(
|
|
||||||
ignoreUserResult = ignoreUserSuccess
|
|
||||||
).apply {
|
|
||||||
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteSuccess))
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
client = client,
|
|
||||||
notificationCleaner = fakeNotificationCleaner,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isInstanceOf(AsyncAction.Success::class.java)
|
|
||||||
}
|
|
||||||
cancelAndConsumeRemainingEvents()
|
|
||||||
}
|
|
||||||
declineInviteSuccess.assertions().isCalledOnce()
|
|
||||||
ignoreUserSuccess.assertions().isCalledOnce().with(value(A_USER_ID))
|
|
||||||
clearMembershipNotificationForRoomLambda.assertions()
|
|
||||||
.isCalledOnce()
|
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - declining invite with block error flow`() = runTest {
|
|
||||||
val declineInviteFailure = lambdaRecorder { ->
|
|
||||||
Result.failure<Unit>(RuntimeException("Failed to leave room"))
|
|
||||||
}
|
|
||||||
val client = FakeMatrixClient().apply {
|
|
||||||
givenGetRoomResult(A_ROOM_ID, FakeBaseRoom(leaveRoomLambda = declineInviteFailure))
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
client = client,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = true)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.declineAction).isEqualTo(ConfirmingDeclineInvite(inviteData, true))
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.ConfirmDeclineInvite
|
|
||||||
)
|
|
||||||
}
|
|
||||||
assertThat(awaitItem().declineAction.isLoading()).isTrue()
|
|
||||||
}
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - accepting invite error flow`() = runTest {
|
|
||||||
val joinRoomFailure = lambdaRecorder { roomIdOrAlias: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
|
|
||||||
Result.failure<Unit>(RuntimeException("Failed to join room $roomIdOrAlias"))
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
joinRoomLambda = joinRoomFailure,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Failure::class.java)
|
|
||||||
state.eventSink(
|
|
||||||
InternalAcceptDeclineInviteEvents.DismissAcceptError
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Uninitialized::class.java)
|
|
||||||
}
|
|
||||||
cancelAndConsumeRemainingEvents()
|
|
||||||
}
|
|
||||||
assert(joinRoomFailure)
|
|
||||||
.isCalledOnce()
|
|
||||||
.with(
|
|
||||||
value(A_ROOM_ID.toRoomIdOrAlias()),
|
|
||||||
value(emptyList<String>()),
|
|
||||||
value(JoinedRoom.Trigger.Invite)
|
|
||||||
)
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun `present - accepting invite success flow`() = runTest {
|
|
||||||
val clearMembershipNotificationForRoomLambda = lambdaRecorder<SessionId, RoomId, Unit> { _, _ ->
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val fakeNotificationCleaner = FakeNotificationCleaner(
|
|
||||||
clearMembershipNotificationForRoomLambda = clearMembershipNotificationForRoomLambda
|
|
||||||
)
|
|
||||||
val joinRoomSuccess = lambdaRecorder { _: RoomIdOrAlias, _: List<String>, _: JoinedRoom.Trigger ->
|
|
||||||
Result.success(Unit)
|
|
||||||
}
|
|
||||||
val seenInvitesStore = InMemorySeenInvitesStore(setOf(A_ROOM_ID, A_ROOM_ID_2, A_ROOM_ID_3))
|
|
||||||
val presenter = createAcceptDeclineInvitePresenter(
|
|
||||||
joinRoomLambda = joinRoomSuccess,
|
|
||||||
notificationCleaner = fakeNotificationCleaner,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
presenter.test {
|
|
||||||
val inviteData = anInviteData()
|
|
||||||
awaitItem().also { state ->
|
|
||||||
state.eventSink(
|
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isEqualTo(AsyncAction.Loading)
|
|
||||||
}
|
|
||||||
awaitItem().also { state ->
|
|
||||||
assertThat(state.acceptAction).isInstanceOf(AsyncAction.Success::class.java)
|
|
||||||
}
|
|
||||||
cancelAndConsumeRemainingEvents()
|
|
||||||
}
|
|
||||||
assert(joinRoomSuccess)
|
|
||||||
.isCalledOnce()
|
|
||||||
.with(
|
|
||||||
value(A_ROOM_ID.toRoomIdOrAlias()),
|
|
||||||
value(emptyList<String>()),
|
|
||||||
value(JoinedRoom.Trigger.Invite)
|
|
||||||
)
|
|
||||||
clearMembershipNotificationForRoomLambda.assertions()
|
|
||||||
.isCalledOnce()
|
|
||||||
.with(value(A_SESSION_ID), value(A_ROOM_ID))
|
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(A_ROOM_ID_2, A_ROOM_ID_3)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun anInviteData(
|
|
||||||
roomId: RoomId = A_ROOM_ID,
|
|
||||||
name: String = A_ROOM_NAME,
|
|
||||||
isDm: Boolean = false,
|
|
||||||
senderId: UserId = A_USER_ID,
|
|
||||||
): InviteData {
|
|
||||||
return InviteData(
|
|
||||||
roomId = roomId,
|
|
||||||
roomName = name,
|
|
||||||
isDm = isDm,
|
|
||||||
senderId = senderId,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createAcceptDeclineInvitePresenter(
|
|
||||||
client: MatrixClient = FakeMatrixClient(),
|
|
||||||
joinRoomLambda: (RoomIdOrAlias, List<String>, JoinedRoom.Trigger) -> Result<Unit> = { _, _, _ ->
|
|
||||||
Result.success(Unit)
|
|
||||||
},
|
|
||||||
notificationCleaner: NotificationCleaner = FakeNotificationCleaner(),
|
|
||||||
seenInvitesStore: SeenInvitesStore = InMemorySeenInvitesStore(),
|
|
||||||
): AcceptDeclineInvitePresenter {
|
|
||||||
return AcceptDeclineInvitePresenter(
|
|
||||||
client = client,
|
|
||||||
joinRoom = FakeJoinRoom(joinRoomLambda),
|
|
||||||
notificationCleaner = notificationCleaner,
|
|
||||||
seenInvitesStore = seenInvitesStore,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -25,5 +25,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.coroutines.core)
|
implementation(libs.coroutines.core)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrix.test)
|
||||||
api(projects.features.invite.api)
|
api(projects.features.invite.api)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.invite.test
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||||
|
|
||||||
|
fun anInviteData(
|
||||||
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
roomName: String = A_ROOM_NAME,
|
||||||
|
isDm: Boolean = false,
|
||||||
|
) = InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = roomName,
|
||||||
|
isDm = isDm,
|
||||||
|
)
|
||||||
|
|
@ -36,6 +36,7 @@ dependencies {
|
||||||
implementation(projects.features.roomdirectory.api)
|
implementation(projects.features.roomdirectory.api)
|
||||||
implementation(projects.services.analytics.api)
|
implementation(projects.services.analytics.api)
|
||||||
implementation(projects.libraries.preferences.api)
|
implementation(projects.libraries.preferences.api)
|
||||||
|
implementation(projects.appconfig)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import javax.inject.Inject
|
||||||
@ContributesBinding(AppScope::class)
|
@ContributesBinding(AppScope::class)
|
||||||
class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint {
|
class DefaultJoinRoomEntryPoint @Inject constructor() : JoinRoomEntryPoint {
|
||||||
override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node {
|
override fun createNode(parentNode: Node, buildContext: BuildContext, inputs: JoinRoomEntryPoint.Inputs): Node {
|
||||||
return parentNode.createNode<JoinRoomNode>(
|
return parentNode.createNode<JoinRoomFlowNode>(
|
||||||
buildContext = buildContext,
|
buildContext = buildContext,
|
||||||
plugins = listOf(inputs)
|
plugins = listOf(inputs)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
|
||||||
sealed interface JoinRoomEvents {
|
sealed interface JoinRoomEvents {
|
||||||
data object RetryFetchingContent : JoinRoomEvents
|
data object RetryFetchingContent : JoinRoomEvents
|
||||||
data object DismissErrorAndHideContent : JoinRoomEvents
|
data object DismissErrorAndHideContent : JoinRoomEvents
|
||||||
|
|
@ -16,6 +18,6 @@ sealed interface JoinRoomEvents {
|
||||||
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
|
data class CancelKnock(val requiresConfirmation: Boolean) : JoinRoomEvents
|
||||||
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
|
data class UpdateKnockMessage(val message: String) : JoinRoomEvents
|
||||||
data object ClearActionStates : JoinRoomEvents
|
data object ClearActionStates : JoinRoomEvents
|
||||||
data object AcceptInvite : JoinRoomEvents
|
data class AcceptInvite(val inviteData: InviteData) : JoinRoomEvents
|
||||||
data class DeclineInvite(val blockUser: Boolean) : JoinRoomEvents
|
data class DeclineInvite(val inviteData: InviteData, val blockUser: Boolean) : JoinRoomEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2024 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.node.node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||||
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteView
|
||||||
|
import io.element.android.features.invite.api.declineandblock.DeclineInviteAndBlockEntryPoint
|
||||||
|
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.BackstackView
|
||||||
|
import io.element.android.libraries.architecture.BaseFlowNode
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class JoinRoomFlowNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: JoinRoomPresenter.Factory,
|
||||||
|
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||||
|
private val declineAndBlockEntryPoint: DeclineInviteAndBlockEntryPoint
|
||||||
|
) : BaseFlowNode<JoinRoomFlowNode.NavTarget>(
|
||||||
|
backstack = BackStack(
|
||||||
|
initialElement = NavTarget.Root,
|
||||||
|
savedStateMap = buildContext.savedStateMap,
|
||||||
|
),
|
||||||
|
buildContext = buildContext,
|
||||||
|
plugins = plugins
|
||||||
|
) {
|
||||||
|
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
||||||
|
private val presenter = presenterFactory.create(
|
||||||
|
inputs.roomId,
|
||||||
|
inputs.roomIdOrAlias,
|
||||||
|
inputs.roomDescription,
|
||||||
|
inputs.serverNames,
|
||||||
|
inputs.trigger,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface NavTarget : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data object Root : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class DeclineInviteAndBlockUser(val inviteData: InviteData) : NavTarget
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
return when (navTarget) {
|
||||||
|
is NavTarget.DeclineInviteAndBlockUser -> declineAndBlockEntryPoint.createNode(this, buildContext, navTarget.inviteData)
|
||||||
|
NavTarget.Root -> rootNode(buildContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
BackstackView(modifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rootNode(buildContext: BuildContext): Node {
|
||||||
|
return node(buildContext) { modifier ->
|
||||||
|
val state = presenter.present()
|
||||||
|
JoinRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = ::navigateUp,
|
||||||
|
onJoinSuccess = ::navigateUp,
|
||||||
|
onForgetSuccess = ::navigateUp,
|
||||||
|
onCancelKnockSuccess = {},
|
||||||
|
onKnockSuccess = {},
|
||||||
|
onDeclineInviteAndBlockUser = {
|
||||||
|
backstack.push(
|
||||||
|
NavTarget.DeclineInviteAndBlockUser(it)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
acceptDeclineInviteView.Render(
|
||||||
|
state = state.acceptDeclineInviteState,
|
||||||
|
onAcceptInviteSuccess = {},
|
||||||
|
onDeclineInviteSuccess = {},
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright 2024 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.features.joinroom.impl
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
|
||||||
import com.bumble.appyx.core.node.Node
|
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
|
||||||
import io.element.android.features.joinroom.api.JoinRoomEntryPoint
|
|
||||||
import io.element.android.libraries.architecture.inputs
|
|
||||||
import io.element.android.libraries.di.SessionScope
|
|
||||||
|
|
||||||
@ContributesNode(SessionScope::class)
|
|
||||||
class JoinRoomNode @AssistedInject constructor(
|
|
||||||
@Assisted buildContext: BuildContext,
|
|
||||||
@Assisted plugins: List<Plugin>,
|
|
||||||
presenterFactory: JoinRoomPresenter.Factory,
|
|
||||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
|
||||||
) : Node(buildContext, plugins = plugins) {
|
|
||||||
private val inputs: JoinRoomEntryPoint.Inputs = inputs()
|
|
||||||
private val presenter = presenterFactory.create(
|
|
||||||
inputs.roomId,
|
|
||||||
inputs.roomIdOrAlias,
|
|
||||||
inputs.roomDescription,
|
|
||||||
inputs.serverNames,
|
|
||||||
inputs.trigger,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun View(modifier: Modifier) {
|
|
||||||
val state = presenter.present()
|
|
||||||
JoinRoomView(
|
|
||||||
state = state,
|
|
||||||
onBackClick = ::navigateUp,
|
|
||||||
onJoinSuccess = ::navigateUp,
|
|
||||||
onForgetSuccess = ::navigateUp,
|
|
||||||
onCancelKnockSuccess = {},
|
|
||||||
onKnockSuccess = {},
|
|
||||||
modifier = modifier
|
|
||||||
)
|
|
||||||
acceptDeclineInviteView.Render(
|
|
||||||
state = state.acceptDeclineInviteState,
|
|
||||||
onAcceptInvite = {},
|
|
||||||
onDeclineInvite = {},
|
|
||||||
modifier = Modifier
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -23,10 +23,11 @@ import androidx.compose.runtime.setValue
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
|
import io.element.android.appconfig.MatrixConfiguration
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.InviteData
|
import io.element.android.features.invite.api.toInviteData
|
||||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||||
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
||||||
import io.element.android.features.joinroom.impl.di.KnockRoom
|
import io.element.android.features.joinroom.impl.di.KnockRoom
|
||||||
|
|
@ -170,16 +171,14 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
when (event) {
|
when (event) {
|
||||||
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
|
JoinRoomEvents.JoinRoom -> coroutineScope.joinRoom(joinAction)
|
||||||
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
|
is JoinRoomEvents.KnockRoom -> coroutineScope.knockRoom(knockAction, knockMessage)
|
||||||
JoinRoomEvents.AcceptInvite -> {
|
is JoinRoomEvents.AcceptInvite -> {
|
||||||
val inviteData = contentState.toInviteData()
|
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptDeclineInviteEvents.AcceptInvite(inviteData)
|
AcceptDeclineInviteEvents.AcceptInvite(event.inviteData)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is JoinRoomEvents.DeclineInvite -> {
|
is JoinRoomEvents.DeclineInvite -> {
|
||||||
val inviteData = contentState.toInviteData()
|
|
||||||
acceptDeclineInviteState.eventSink(
|
acceptDeclineInviteState.eventSink(
|
||||||
AcceptDeclineInviteEvents.DeclineInvite(invite = inviteData, blockUser = event.blockUser)
|
AcceptDeclineInviteEvents.DeclineInvite(invite = event.inviteData, blockUser = event.blockUser, shouldConfirm = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
|
is JoinRoomEvents.CancelKnock -> coroutineScope.cancelKnockRoom(event.requiresConfirmation, cancelKnockAction)
|
||||||
|
|
@ -213,6 +212,7 @@ class JoinRoomPresenter @AssistedInject constructor(
|
||||||
applicationName = buildMeta.applicationName,
|
applicationName = buildMeta.applicationName,
|
||||||
knockMessage = knockMessage,
|
knockMessage = knockMessage,
|
||||||
hideInviteAvatars = hideInviteAvatars,
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
|
canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM,
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +273,12 @@ private fun RoomPreviewInfo.toContentState(senderMember: RoomMember?, reason: St
|
||||||
roomType = roomType,
|
roomType = roomType,
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (membership) {
|
joinAuthorisationStatus = when (membership) {
|
||||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(senderMember?.toInviteSender())
|
CurrentUserMembership.INVITED -> {
|
||||||
|
JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteData = toInviteData(),
|
||||||
|
inviteSender = senderMember?.toInviteSender()
|
||||||
|
)
|
||||||
|
}
|
||||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
|
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(senderMember?.toInviteSender(), reason)
|
||||||
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
CurrentUserMembership.KNOCKED -> JoinAuthorisationStatus.IsKnocked
|
||||||
else -> joinRule.toJoinAuthorisationStatus()
|
else -> joinRule.toJoinAuthorisationStatus()
|
||||||
|
|
@ -317,7 +322,8 @@ internal fun RoomInfo.toContentState(
|
||||||
roomAvatarUrl = avatarUrl,
|
roomAvatarUrl = avatarUrl,
|
||||||
joinAuthorisationStatus = when (currentUserMembership) {
|
joinAuthorisationStatus = when (currentUserMembership) {
|
||||||
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
CurrentUserMembership.INVITED -> JoinAuthorisationStatus.IsInvited(
|
||||||
inviteSender = membershipSender?.toInviteSender()
|
inviteData = toInviteData(),
|
||||||
|
inviteSender = membershipSender?.toInviteSender(),
|
||||||
)
|
)
|
||||||
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
|
CurrentUserMembership.BANNED -> JoinAuthorisationStatus.IsBanned(
|
||||||
banSender = membershipSender?.toInviteSender(),
|
banSender = membershipSender?.toInviteSender(),
|
||||||
|
|
@ -340,23 +346,3 @@ private fun JoinRule?.toJoinAuthorisationStatus(): JoinAuthorisationStatus {
|
||||||
else -> JoinAuthorisationStatus.Unknown
|
else -> JoinAuthorisationStatus.Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun ContentState.toInviteData(): InviteData? {
|
|
||||||
return when (this) {
|
|
||||||
is ContentState.Loaded -> {
|
|
||||||
if (joinAuthorisationStatus is JoinAuthorisationStatus.IsInvited && joinAuthorisationStatus.inviteSender != null) {
|
|
||||||
InviteData(
|
|
||||||
roomId = roomId,
|
|
||||||
// Note: name should not be null at this point, but use Id just in case...
|
|
||||||
roomName = name ?: roomId.value,
|
|
||||||
senderId = joinAuthorisationStatus.inviteSender.userId,
|
|
||||||
isDm = isDm
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -32,6 +33,7 @@ data class JoinRoomState(
|
||||||
private val applicationName: String,
|
private val applicationName: String,
|
||||||
val knockMessage: String,
|
val knockMessage: String,
|
||||||
val hideInviteAvatars: Boolean,
|
val hideInviteAvatars: Boolean,
|
||||||
|
val canReportRoom: Boolean,
|
||||||
val eventSink: (JoinRoomEvents) -> Unit
|
val eventSink: (JoinRoomEvents) -> Unit
|
||||||
) {
|
) {
|
||||||
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
|
val isJoinActionUnauthorized = joinAction is AsyncAction.Failure && joinAction.error is JoinRoomFailures.UnauthorizedJoin
|
||||||
|
|
@ -95,7 +97,7 @@ sealed interface ContentState {
|
||||||
sealed interface JoinAuthorisationStatus {
|
sealed interface JoinAuthorisationStatus {
|
||||||
data object None : JoinAuthorisationStatus
|
data object None : JoinAuthorisationStatus
|
||||||
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
|
data class IsSpace(val applicationName: String) : JoinAuthorisationStatus
|
||||||
data class IsInvited(val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
data class IsInvited(val inviteData: InviteData, val inviteSender: InviteSender?) : JoinAuthorisationStatus
|
||||||
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
|
data class IsBanned(val banSender: InviteSender?, val reason: String?) : JoinAuthorisationStatus
|
||||||
data object IsKnocked : JoinAuthorisationStatus
|
data object IsKnocked : JoinAuthorisationStatus
|
||||||
data object CanKnock : JoinAuthorisationStatus
|
data object CanKnock : JoinAuthorisationStatus
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@
|
||||||
package io.element.android.features.joinroom.impl
|
package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -50,12 +51,20 @@ open class JoinRoomStateProvider : PreviewParameterProvider<JoinRoomState> {
|
||||||
joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong", null))
|
joinAction = AsyncAction.Failure(ClientException.Generic("Something went wrong", null))
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null))
|
contentState = aLoadedContentState(
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteData = anInviteData(),
|
||||||
|
inviteSender = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(
|
contentState = aLoadedContentState(
|
||||||
numberOfMembers = 123,
|
numberOfMembers = 123,
|
||||||
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(anInviteSender()),
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteData = anInviteData(),
|
||||||
|
inviteSender = anInviteSender(),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
|
|
@ -149,7 +158,7 @@ fun aLoadedContentState(
|
||||||
isDm: Boolean = false,
|
isDm: Boolean = false,
|
||||||
roomType: RoomType = RoomType.Room,
|
roomType: RoomType = RoomType.Room,
|
||||||
roomAvatarUrl: String? = null,
|
roomAvatarUrl: String? = null,
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown
|
joinAuthorisationStatus: JoinAuthorisationStatus = JoinAuthorisationStatus.Unknown,
|
||||||
) = ContentState.Loaded(
|
) = ContentState.Loaded(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
name = name,
|
name = name,
|
||||||
|
|
@ -172,6 +181,7 @@ fun aJoinRoomState(
|
||||||
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
cancelKnockAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
knockMessage: String = "",
|
knockMessage: String = "",
|
||||||
hideInviteAvatars: Boolean = false,
|
hideInviteAvatars: Boolean = false,
|
||||||
|
canReportRoom: Boolean = true,
|
||||||
eventSink: (JoinRoomEvents) -> Unit = {}
|
eventSink: (JoinRoomEvents) -> Unit = {}
|
||||||
) = JoinRoomState(
|
) = JoinRoomState(
|
||||||
roomIdOrAlias = roomIdOrAlias,
|
roomIdOrAlias = roomIdOrAlias,
|
||||||
|
|
@ -184,6 +194,7 @@ fun aJoinRoomState(
|
||||||
applicationName = "AppName",
|
applicationName = "AppName",
|
||||||
knockMessage = knockMessage,
|
knockMessage = knockMessage,
|
||||||
hideInviteAvatars = hideInviteAvatars,
|
hideInviteAvatars = hideInviteAvatars,
|
||||||
|
canReportRoom = canReportRoom,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -199,5 +210,15 @@ internal fun anInviteSender(
|
||||||
membershipChangeReason = membershipChangeReason,
|
membershipChangeReason = membershipChangeReason,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal fun anInviteData(
|
||||||
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
roomName: String = "Room name",
|
||||||
|
isDm: Boolean = false,
|
||||||
|
) = InviteData(
|
||||||
|
roomId = roomId,
|
||||||
|
roomName = roomName,
|
||||||
|
isDm = isDm,
|
||||||
|
)
|
||||||
|
|
||||||
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
private val A_ROOM_ID = RoomId("!exa:matrix.org")
|
||||||
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
private val A_ROOM_ALIAS = RoomAlias("#exa:matrix.org")
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewDescriptionAtom
|
||||||
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
import io.element.android.libraries.designsystem.atomic.atoms.RoomPreviewSubtitleAtom
|
||||||
|
|
@ -78,6 +79,7 @@ fun JoinRoomView(
|
||||||
onKnockSuccess: () -> Unit,
|
onKnockSuccess: () -> Unit,
|
||||||
onForgetSuccess: () -> Unit,
|
onForgetSuccess: () -> Unit,
|
||||||
onCancelKnockSuccess: () -> Unit,
|
onCancelKnockSuccess: () -> Unit,
|
||||||
|
onDeclineInviteAndBlockUser: (InviteData) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -104,11 +106,15 @@ fun JoinRoomView(
|
||||||
footer = {
|
footer = {
|
||||||
JoinRoomFooter(
|
JoinRoomFooter(
|
||||||
joinAuthorisationStatus = state.joinAuthorisationStatus,
|
joinAuthorisationStatus = state.joinAuthorisationStatus,
|
||||||
onAcceptInvite = {
|
onAcceptInvite = { inviteData ->
|
||||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
|
||||||
},
|
},
|
||||||
onDeclineInvite = { blockUser ->
|
onDeclineInvite = { inviteData, blockUser ->
|
||||||
state.eventSink(JoinRoomEvents.DeclineInvite(blockUser))
|
if (state.canReportRoom && blockUser) {
|
||||||
|
onDeclineInviteAndBlockUser(inviteData)
|
||||||
|
} else {
|
||||||
|
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, blockUser = blockUser))
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onJoinRoom = {
|
onJoinRoom = {
|
||||||
state.eventSink(JoinRoomEvents.JoinRoom)
|
state.eventSink(JoinRoomEvents.JoinRoom)
|
||||||
|
|
@ -184,8 +190,8 @@ fun JoinRoomView(
|
||||||
@Composable
|
@Composable
|
||||||
private fun JoinRoomFooter(
|
private fun JoinRoomFooter(
|
||||||
joinAuthorisationStatus: JoinAuthorisationStatus,
|
joinAuthorisationStatus: JoinAuthorisationStatus,
|
||||||
onAcceptInvite: () -> Unit,
|
onAcceptInvite: (InviteData) -> Unit,
|
||||||
onDeclineInvite: (Boolean) -> Unit,
|
onDeclineInvite: (InviteData, Boolean) -> Unit,
|
||||||
onJoinRoom: () -> Unit,
|
onJoinRoom: () -> Unit,
|
||||||
onKnockRoom: () -> Unit,
|
onKnockRoom: () -> Unit,
|
||||||
onCancelKnock: () -> Unit,
|
onCancelKnock: () -> Unit,
|
||||||
|
|
@ -204,13 +210,13 @@ private fun JoinRoomFooter(
|
||||||
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
ButtonRowMolecule(horizontalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
text = stringResource(CommonStrings.action_decline),
|
text = stringResource(CommonStrings.action_decline),
|
||||||
onClick = { onDeclineInvite(false) },
|
onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, false) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.LargeLowPadding,
|
size = ButtonSize.LargeLowPadding,
|
||||||
)
|
)
|
||||||
Button(
|
Button(
|
||||||
text = stringResource(CommonStrings.action_accept),
|
text = stringResource(CommonStrings.action_accept),
|
||||||
onClick = onAcceptInvite,
|
onClick = { onAcceptInvite(joinAuthorisationStatus.inviteData) },
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
size = ButtonSize.LargeLowPadding,
|
size = ButtonSize.LargeLowPadding,
|
||||||
)
|
)
|
||||||
|
|
@ -218,7 +224,7 @@ private fun JoinRoomFooter(
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
TextButton(
|
TextButton(
|
||||||
text = stringResource(R.string.screen_join_room_decline_and_block_button_title),
|
text = stringResource(R.string.screen_join_room_decline_and_block_button_title),
|
||||||
onClick = { onDeclineInvite(true) },
|
onClick = { onDeclineInvite(joinAuthorisationStatus.inviteData, true) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
destructive = true
|
destructive = true
|
||||||
)
|
)
|
||||||
|
|
@ -585,5 +591,6 @@ internal fun JoinRoomViewPreview(@PreviewParameter(JoinRoomStateProvider::class)
|
||||||
onKnockSuccess = { },
|
onKnockSuccess = { },
|
||||||
onForgetSuccess = { },
|
onForgetSuccess = { },
|
||||||
onCancelKnockSuccess = { },
|
onCancelKnockSuccess = { },
|
||||||
|
onDeclineInviteAndBlockUser = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
import io.element.android.features.joinroom.impl.JoinRoomPresenter
|
||||||
import io.element.android.features.roomdirectory.api.RoomDescription
|
import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ package io.element.android.features.joinroom.impl
|
||||||
|
|
||||||
import com.google.common.truth.Truth.assertThat
|
import com.google.common.truth.Truth.assertThat
|
||||||
import im.vector.app.features.analytics.plan.JoinedRoom
|
import im.vector.app.features.analytics.plan.JoinedRoom
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
import io.element.android.features.invite.api.SeenInvitesStore
|
import io.element.android.features.invite.api.SeenInvitesStore
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteEvents
|
||||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.AcceptDeclineInviteState
|
||||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
import io.element.android.features.invite.api.acceptdecline.anAcceptDeclineInviteState
|
||||||
|
import io.element.android.features.invite.api.toInviteData
|
||||||
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
import io.element.android.features.invite.test.InMemorySeenInvitesStore
|
||||||
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
import io.element.android.features.joinroom.impl.di.CancelKnockRoom
|
||||||
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
import io.element.android.features.joinroom.impl.di.ForgetRoom
|
||||||
|
|
@ -21,6 +23,8 @@ import io.element.android.features.roomdirectory.api.RoomDescription
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
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.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
import io.element.android.libraries.matrix.api.core.RoomAlias
|
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.RoomId
|
||||||
|
|
@ -46,6 +50,7 @@ import io.element.android.libraries.matrix.test.room.aRoomPreview
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
import io.element.android.libraries.matrix.test.room.aRoomPreviewInfo
|
||||||
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
import io.element.android.libraries.matrix.test.room.aRoomSummary
|
||||||
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
import io.element.android.libraries.matrix.test.room.join.FakeJoinRoom
|
||||||
|
import io.element.android.libraries.matrix.ui.model.InviteSender
|
||||||
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||||
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore
|
||||||
|
|
@ -124,11 +129,12 @@ class JoinRoomPresenterTest {
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
seenInvitesStore = seenInvitesStore,
|
seenInvitesStore = seenInvitesStore,
|
||||||
)
|
)
|
||||||
|
val inviteData = roomSummary.info.toInviteData()
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
assertThat(seenInvitesStore.seenRoomIds().first()).isEmpty()
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(null))
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, null))
|
||||||
}
|
}
|
||||||
// Check that the roomId is stored in the seen invites store
|
// Check that the roomId is stored in the seen invites store
|
||||||
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomSummary.roomId)
|
assertThat(seenInvitesStore.seenRoomIds().first()).containsExactly(roomSummary.roomId)
|
||||||
|
|
@ -144,6 +150,7 @@ class JoinRoomPresenterTest {
|
||||||
joinedMembersCount = 5,
|
joinedMembersCount = 5,
|
||||||
inviter = inviter,
|
inviter = inviter,
|
||||||
)
|
)
|
||||||
|
val inviteData = roomSummary.info.toInviteData()
|
||||||
val matrixClient = FakeMatrixClient(
|
val matrixClient = FakeMatrixClient(
|
||||||
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
|
getNotJoinedRoomResult = { _, _ -> Result.failure(AN_EXCEPTION) },
|
||||||
).apply {
|
).apply {
|
||||||
|
|
@ -157,7 +164,7 @@ class JoinRoomPresenterTest {
|
||||||
presenter.test {
|
presenter.test {
|
||||||
skipItems(2)
|
skipItems(2)
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(expectedInviteSender))
|
assertThat(state.joinAuthorisationStatus).isEqualTo(JoinAuthorisationStatus.IsInvited(inviteData, expectedInviteSender))
|
||||||
assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5)
|
assertThat((state.contentState as ContentState.Loaded).numberOfMembers).isEqualTo(5)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +215,7 @@ class JoinRoomPresenterTest {
|
||||||
flowOf(Optional.of(roomSummary))
|
flowOf(Optional.of(roomSummary))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val inviteData = roomSummary.info.toInviteData()
|
||||||
val presenter = createJoinRoomPresenter(
|
val presenter = createJoinRoomPresenter(
|
||||||
matrixClient = matrixClient,
|
matrixClient = matrixClient,
|
||||||
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
||||||
|
|
@ -216,16 +224,14 @@ class JoinRoomPresenterTest {
|
||||||
skipItems(1)
|
skipItems(1)
|
||||||
|
|
||||||
awaitItem().also { state ->
|
awaitItem().also { state ->
|
||||||
state.eventSink(JoinRoomEvents.AcceptInvite)
|
state.eventSink(JoinRoomEvents.AcceptInvite(inviteData))
|
||||||
state.eventSink(JoinRoomEvents.DeclineInvite(false))
|
state.eventSink(JoinRoomEvents.DeclineInvite(inviteData, false))
|
||||||
|
|
||||||
val inviteData = state.contentState.toInviteData()
|
|
||||||
|
|
||||||
assert(eventSinkRecorder)
|
assert(eventSinkRecorder)
|
||||||
.isCalledExactly(2)
|
.isCalledExactly(2)
|
||||||
.withSequence(
|
.withSequence(
|
||||||
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
|
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
|
||||||
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))),
|
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData, blockUser = false, shouldConfirm = true))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -613,7 +619,7 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is not known RoomPreview is loaded`() = runTest {
|
fun `present - when room is not known RoomPreview is loaded - membership null`() = runTest {
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
getNotJoinedRoomResult = { _, _ ->
|
getNotJoinedRoomResult = { _, _ ->
|
||||||
Result.success(
|
Result.success(
|
||||||
|
|
@ -657,6 +663,193 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded - membership INVITED`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getNotJoinedRoomResult = { _, _ ->
|
||||||
|
Result.success(
|
||||||
|
aRoomPreview(
|
||||||
|
info = aRoomPreviewInfo(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
avatarUrl = "avatarUrl",
|
||||||
|
numberOfJoinedMembers = 2,
|
||||||
|
isSpace = false,
|
||||||
|
isHistoryWorldReadable = false,
|
||||||
|
joinRule = JoinRule.Public,
|
||||||
|
currentUserMembership = CurrentUserMembership.INVITED,
|
||||||
|
),
|
||||||
|
roomMembershipDetails = {
|
||||||
|
Result.success(
|
||||||
|
RoomMembershipDetails(
|
||||||
|
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
|
||||||
|
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loaded(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
|
numberOfMembers = 2,
|
||||||
|
isDm = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
roomAvatarUrl = "avatarUrl",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(
|
||||||
|
inviteData = InviteData(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
roomName = "Room name",
|
||||||
|
isDm = false,
|
||||||
|
),
|
||||||
|
inviteSender = InviteSender(
|
||||||
|
userId = A_USER_ID_2,
|
||||||
|
displayName = "Bob",
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = A_USER_ID_2.value,
|
||||||
|
name = "Bob",
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
|
),
|
||||||
|
membershipChangeReason = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded - membership BANNED`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getNotJoinedRoomResult = { _, _ ->
|
||||||
|
Result.success(
|
||||||
|
aRoomPreview(
|
||||||
|
info = aRoomPreviewInfo(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
name = null,
|
||||||
|
topic = "Room topic",
|
||||||
|
avatarUrl = "avatarUrl",
|
||||||
|
numberOfJoinedMembers = 2,
|
||||||
|
isSpace = false,
|
||||||
|
isHistoryWorldReadable = false,
|
||||||
|
joinRule = JoinRule.Public,
|
||||||
|
currentUserMembership = CurrentUserMembership.BANNED,
|
||||||
|
),
|
||||||
|
roomMembershipDetails = {
|
||||||
|
Result.success(
|
||||||
|
RoomMembershipDetails(
|
||||||
|
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
|
||||||
|
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loaded(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = null,
|
||||||
|
topic = "Room topic",
|
||||||
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
|
numberOfMembers = 2,
|
||||||
|
isDm = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
roomAvatarUrl = "avatarUrl",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsBanned(
|
||||||
|
banSender = InviteSender(
|
||||||
|
userId = A_USER_ID_2,
|
||||||
|
displayName = "Bob",
|
||||||
|
avatarData = AvatarData(
|
||||||
|
id = A_USER_ID_2.value,
|
||||||
|
name = "Bob",
|
||||||
|
size = AvatarSize.InviteSender,
|
||||||
|
),
|
||||||
|
membershipChangeReason = null,
|
||||||
|
),
|
||||||
|
reason = null,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded - membership KNOCKED`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getNotJoinedRoomResult = { _, _ ->
|
||||||
|
Result.success(
|
||||||
|
aRoomPreview(
|
||||||
|
info = aRoomPreviewInfo(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
canonicalAlias = RoomAlias("#alias:matrix.org"),
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
avatarUrl = "avatarUrl",
|
||||||
|
numberOfJoinedMembers = 2,
|
||||||
|
isSpace = false,
|
||||||
|
isHistoryWorldReadable = false,
|
||||||
|
joinRule = JoinRule.Public,
|
||||||
|
currentUserMembership = CurrentUserMembership.KNOCKED,
|
||||||
|
),
|
||||||
|
roomMembershipDetails = {
|
||||||
|
Result.success(
|
||||||
|
RoomMembershipDetails(
|
||||||
|
currentUserMember = aRoomMember(userId = A_USER_ID, displayName = "Alice"),
|
||||||
|
senderMember = aRoomMember(userId = A_USER_ID_2, displayName = "Bob"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Loaded(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
name = "Room name",
|
||||||
|
topic = "Room topic",
|
||||||
|
alias = RoomAlias("#alias:matrix.org"),
|
||||||
|
numberOfMembers = 2,
|
||||||
|
isDm = false,
|
||||||
|
roomType = RoomType.Room,
|
||||||
|
roomAvatarUrl = "avatarUrl",
|
||||||
|
joinAuthorisationStatus = JoinAuthorisationStatus.IsKnocked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is not known RoomPreview is loaded as Private`() = runTest {
|
fun `present - when room is not known RoomPreview is loaded as Private`() = runTest {
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
|
|
@ -807,6 +1000,31 @@ class JoinRoomPresenterTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - when room is not known RoomPreview is loaded with error - dismiss`() = runTest {
|
||||||
|
val client = FakeMatrixClient(
|
||||||
|
getNotJoinedRoomResult = { _, _ ->
|
||||||
|
Result.failure(AN_EXCEPTION)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val presenter = createJoinRoomPresenter(
|
||||||
|
matrixClient = client
|
||||||
|
)
|
||||||
|
presenter.test {
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(
|
||||||
|
ContentState.Failure(error = AN_EXCEPTION)
|
||||||
|
)
|
||||||
|
state.eventSink(JoinRoomEvents.DismissErrorAndHideContent)
|
||||||
|
}
|
||||||
|
skipItems(1)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.contentState).isEqualTo(ContentState.Dismissing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest {
|
fun `present - when room is not known RoomPreview is loaded with error Forbidden`() = runTest {
|
||||||
val client = FakeMatrixClient(
|
val client = FakeMatrixClient(
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,19 @@ import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import io.element.android.features.invite.api.InviteData
|
||||||
|
import io.element.android.features.invite.test.anInviteData
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.matrix.api.room.RoomType
|
import io.element.android.libraries.matrix.api.room.RoomType
|
||||||
|
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||||
|
import io.element.android.libraries.matrix.ui.model.toInviteSender
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||||
|
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||||
import io.element.android.tests.testutils.EventsRecorder
|
import io.element.android.tests.testutils.EventsRecorder
|
||||||
import io.element.android.tests.testutils.clickOn
|
import io.element.android.tests.testutils.clickOn
|
||||||
import io.element.android.tests.testutils.ensureCalledOnce
|
import io.element.android.tests.testutils.ensureCalledOnce
|
||||||
|
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||||
import io.element.android.tests.testutils.pressBack
|
import io.element.android.tests.testutils.pressBack
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
@ -141,40 +147,61 @@ class JoinRoomViewTest {
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
|
fun `clicking on Accept when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
val inviteData = anInviteData()
|
||||||
rule.setJoinRoomView(
|
rule.setJoinRoomView(
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_accept)
|
rule.clickOn(CommonStrings.action_accept)
|
||||||
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite)
|
eventsRecorder.assertSingle(JoinRoomEvents.AcceptInvite(inviteData))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
|
fun `clicking on Decline when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
|
||||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
val inviteData = anInviteData()
|
||||||
rule.setJoinRoomView(
|
rule.setJoinRoomView(
|
||||||
aJoinRoomState(
|
aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, null)),
|
||||||
eventSink = eventsRecorder,
|
eventSink = eventsRecorder,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
rule.clickOn(CommonStrings.action_decline)
|
rule.clickOn(CommonStrings.action_decline)
|
||||||
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(false))
|
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited emits the expected Event`() {
|
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and can report room, the expected callback is invoked`() {
|
||||||
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>(expectEvents = false)
|
||||||
rule.setJoinRoomView(
|
val inviteData = anInviteData()
|
||||||
aJoinRoomState(
|
val joinRoomState = aJoinRoomState(
|
||||||
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(null)),
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
|
||||||
eventSink = eventsRecorder,
|
canReportRoom = true,
|
||||||
),
|
eventSink = eventsRecorder,
|
||||||
)
|
)
|
||||||
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
|
ensureCalledOnceWithParam(inviteData) {
|
||||||
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(true))
|
rule.setJoinRoomView(
|
||||||
|
state = joinRoomState,
|
||||||
|
onDeclineInviteAndBlockUser = it,
|
||||||
|
)
|
||||||
|
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `clicking on Decline and block when JoinAuthorisationStatus is IsInvited and cant report room, emits the expected Event`() {
|
||||||
|
val eventsRecorder = EventsRecorder<JoinRoomEvents>()
|
||||||
|
val inviteData = anInviteData()
|
||||||
|
val joinRoomState = aJoinRoomState(
|
||||||
|
contentState = aLoadedContentState(joinAuthorisationStatus = JoinAuthorisationStatus.IsInvited(inviteData, aRoomMember().toInviteSender())),
|
||||||
|
canReportRoom = false,
|
||||||
|
eventSink = eventsRecorder,
|
||||||
|
)
|
||||||
|
rule.setJoinRoomView(state = joinRoomState,)
|
||||||
|
rule.clickOn(R.string.screen_join_room_decline_and_block_button_title)
|
||||||
|
eventsRecorder.assertSingle(JoinRoomEvents.DeclineInvite(inviteData, true))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -242,6 +269,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||||
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
onKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
onCancelKnockSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
onForgetSuccess: () -> Unit = EnsureNeverCalled(),
|
onForgetSuccess: () -> Unit = EnsureNeverCalled(),
|
||||||
|
onDeclineInviteAndBlockUser: (InviteData) -> Unit = EnsureNeverCalledWithParam(),
|
||||||
) {
|
) {
|
||||||
setContent {
|
setContent {
|
||||||
JoinRoomView(
|
JoinRoomView(
|
||||||
|
|
@ -251,6 +279,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setJoinR
|
||||||
onKnockSuccess = onKnockSuccess,
|
onKnockSuccess = onKnockSuccess,
|
||||||
onForgetSuccess = onForgetSuccess,
|
onForgetSuccess = onForgetSuccess,
|
||||||
onCancelKnockSuccess = onCancelKnockSuccess,
|
onCancelKnockSuccess = onCancelKnockSuccess,
|
||||||
|
onDeclineInviteAndBlockUser = onDeclineInviteAndBlockUser,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ private fun LeaveRoomConfirmationDialog(
|
||||||
is LeaveRoomState.Confirmation.Hidden -> {}
|
is LeaveRoomState.Confirmation.Hidden -> {}
|
||||||
|
|
||||||
is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog(
|
is LeaveRoomState.Confirmation.Dm -> LeaveRoomConfirmationDialog(
|
||||||
text = R.string.leave_conversation_alert_subtitle,
|
text = R.string.leave_room_alert_private_subtitle,
|
||||||
roomId = state.confirmation.roomId,
|
roomId = state.confirmation.roomId,
|
||||||
isDm = true,
|
isDm = false,
|
||||||
eventSink = state.eventSink,
|
eventSink = state.eventSink,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
19
features/reportroom/api/build.gradle.kts
Normal file
19
features/reportroom/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.reportroom.api"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.api
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
interface ReportRoomEntryPoint : FeatureEntryPoint {
|
||||||
|
fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node
|
||||||
|
}
|
||||||
45
features/reportroom/impl/build.gradle.kts
Normal file
45
features/reportroom/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import extension.setupAnvil
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("io.element.android-compose-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "io.element.android.features.reportroom.impl"
|
||||||
|
testOptions {
|
||||||
|
unitTests {
|
||||||
|
isIncludeAndroidResources = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupAnvil()
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api(projects.features.reportroom.api)
|
||||||
|
implementation(projects.libraries.core)
|
||||||
|
implementation(projects.libraries.architecture)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrixui)
|
||||||
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.uiStrings)
|
||||||
|
|
||||||
|
testImplementation(libs.test.junit)
|
||||||
|
testImplementation(libs.coroutines.test)
|
||||||
|
testImplementation(libs.molecule.runtime)
|
||||||
|
testImplementation(libs.test.truth)
|
||||||
|
testImplementation(libs.test.turbine)
|
||||||
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.tests.testutils)
|
||||||
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
testImplementation(libs.test.robolectric)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
|
||||||
|
import io.element.android.libraries.architecture.createNode
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(AppScope::class)
|
||||||
|
class DefaultReportRoomEntryPoint @Inject constructor() : ReportRoomEntryPoint {
|
||||||
|
override fun createNode(parentNode: Node, buildContext: BuildContext, roomId: RoomId): Node {
|
||||||
|
return parentNode.createNode<ReportRoomNode>(buildContext, plugins = listOf(ReportRoomNode.Inputs(roomId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
interface ReportRoom {
|
||||||
|
suspend operator fun invoke(
|
||||||
|
roomId: RoomId,
|
||||||
|
shouldReport: Boolean,
|
||||||
|
reason: String,
|
||||||
|
shouldLeave: Boolean,
|
||||||
|
): Result<Unit>
|
||||||
|
|
||||||
|
sealed class Exception : kotlin.Exception() {
|
||||||
|
data object RoomNotFound : Exception()
|
||||||
|
data object LeftRoomFailed : Exception()
|
||||||
|
data object ReportRoomFailed : Exception()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ContributesBinding(SessionScope::class)
|
||||||
|
class DefaultReportRoom @Inject constructor(
|
||||||
|
private val client: MatrixClient,
|
||||||
|
) : ReportRoom {
|
||||||
|
override suspend operator fun invoke(
|
||||||
|
roomId: RoomId,
|
||||||
|
shouldReport: Boolean,
|
||||||
|
reason: String,
|
||||||
|
shouldLeave: Boolean
|
||||||
|
): Result<Unit> {
|
||||||
|
val room = client.getRoom(roomId)
|
||||||
|
?: return Result.failure(ReportRoom.Exception.RoomNotFound)
|
||||||
|
|
||||||
|
if (shouldReport) {
|
||||||
|
room
|
||||||
|
.reportRoom(reason.takeIf { it.isNotBlank() })
|
||||||
|
.onFailure {
|
||||||
|
return Result.failure(ReportRoom.Exception.ReportRoomFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldLeave) {
|
||||||
|
room
|
||||||
|
.leave()
|
||||||
|
.onFailure {
|
||||||
|
return Result.failure(ReportRoom.Exception.LeftRoomFailed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Result.success(Unit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
sealed interface ReportRoomEvents {
|
||||||
|
data class UpdateReason(val reason: String) : ReportRoomEvents
|
||||||
|
data object ToggleLeaveRoom : ReportRoomEvents
|
||||||
|
data object Report : ReportRoomEvents
|
||||||
|
data object ClearReportAction : ReportRoomEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
import com.bumble.appyx.core.node.Node
|
||||||
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.libraries.architecture.NodeInputs
|
||||||
|
import io.element.android.libraries.architecture.inputs
|
||||||
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
|
||||||
|
@ContributesNode(SessionScope::class)
|
||||||
|
class ReportRoomNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
presenterFactory: ReportRoomPresenter.Factory,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
data class Inputs(val roomId: RoomId) : NodeInputs
|
||||||
|
|
||||||
|
private val roomId = inputs<Inputs>().roomId
|
||||||
|
private val presenter: ReportRoomPresenter = presenterFactory.create(roomId = roomId)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
ReportRoomView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onBackClick = ::navigateUp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import dagger.assisted.Assisted
|
||||||
|
import dagger.assisted.AssistedFactory
|
||||||
|
import dagger.assisted.AssistedInject
|
||||||
|
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.core.RoomId
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class ReportRoomPresenter @AssistedInject constructor(
|
||||||
|
@Assisted private val roomId: RoomId,
|
||||||
|
private val reportRoom: ReportRoom,
|
||||||
|
) : Presenter<ReportRoomState> {
|
||||||
|
@AssistedFactory
|
||||||
|
interface Factory {
|
||||||
|
fun create(roomId: RoomId): ReportRoomPresenter
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ReportRoomState {
|
||||||
|
var reason by rememberSaveable { mutableStateOf("") }
|
||||||
|
var leaveRoom by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var reportAction: MutableState<AsyncAction<Unit>> = remember { mutableStateOf(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
fun handleEvents(event: ReportRoomEvents) {
|
||||||
|
when (event) {
|
||||||
|
ReportRoomEvents.Report -> coroutineScope.reportRoom(reason, leaveRoom, reportAction)
|
||||||
|
ReportRoomEvents.ToggleLeaveRoom -> {
|
||||||
|
leaveRoom = !leaveRoom
|
||||||
|
}
|
||||||
|
is ReportRoomEvents.UpdateReason -> {
|
||||||
|
reason = event.reason
|
||||||
|
}
|
||||||
|
ReportRoomEvents.ClearReportAction -> {
|
||||||
|
reportAction.value = AsyncAction.Uninitialized
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReportRoomState(
|
||||||
|
reason = reason,
|
||||||
|
leaveRoom = leaveRoom,
|
||||||
|
reportAction = reportAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.reportRoom(
|
||||||
|
reason: String,
|
||||||
|
shouldLeave: Boolean,
|
||||||
|
action: MutableState<AsyncAction<Unit>>
|
||||||
|
) = launch {
|
||||||
|
val previousFailure = action.value as? AsyncAction.Failure
|
||||||
|
val shouldReport = previousFailure?.error !is ReportRoom.Exception.LeftRoomFailed
|
||||||
|
runUpdatingState(action) {
|
||||||
|
reportRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
shouldReport = shouldReport,
|
||||||
|
reason = reason,
|
||||||
|
shouldLeave = shouldLeave
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
data class ReportRoomState(
|
||||||
|
val reason: String,
|
||||||
|
val leaveRoom: Boolean,
|
||||||
|
val reportAction: AsyncAction<Unit>,
|
||||||
|
val eventSink: (ReportRoomEvents) -> Unit
|
||||||
|
) {
|
||||||
|
val canReport: Boolean = reason.isNotBlank()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
|
open class ReportRoomStateProvider : PreviewParameterProvider<ReportRoomState> {
|
||||||
|
companion object {
|
||||||
|
private const val A_REPORT_ROOM_REASON = "Inappropriate content"
|
||||||
|
}
|
||||||
|
|
||||||
|
override val values: Sequence<ReportRoomState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aReportRoomState(),
|
||||||
|
aReportRoomState(reason = A_REPORT_ROOM_REASON),
|
||||||
|
aReportRoomState(leaveRoom = true),
|
||||||
|
aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Loading),
|
||||||
|
aReportRoomState(reason = A_REPORT_ROOM_REASON, reportAction = AsyncAction.Failure(Exception("Failed to report"))),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aReportRoomState(
|
||||||
|
reason: String = "",
|
||||||
|
leaveRoom: Boolean = false,
|
||||||
|
reportAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
eventSink: (ReportRoomEvents) -> Unit = {}
|
||||||
|
) = ReportRoomState(
|
||||||
|
reason = reason,
|
||||||
|
leaveRoom = leaveRoom,
|
||||||
|
reportAction = reportAction,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||||
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
|
import io.element.android.libraries.designsystem.components.list.ListItemContent
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
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.ListItem
|
||||||
|
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.TextField
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ReportRoomView(
|
||||||
|
state: ReportRoomState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val isReporting = state.reportAction is AsyncAction.Loading
|
||||||
|
AsyncActionView(
|
||||||
|
async = state.reportAction,
|
||||||
|
onSuccess = { onBackClick() },
|
||||||
|
errorTitle = { failure ->
|
||||||
|
when (failure) {
|
||||||
|
is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_title)
|
||||||
|
else -> stringResource(CommonStrings.dialog_title_error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
errorMessage = { failure ->
|
||||||
|
when (failure) {
|
||||||
|
is ReportRoom.Exception.LeftRoomFailed -> stringResource(R.string.screen_report_room_leave_failed_alert_message)
|
||||||
|
else -> stringResource(CommonStrings.error_unknown)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRetry = {
|
||||||
|
state.eventSink(ReportRoomEvents.Report)
|
||||||
|
},
|
||||||
|
onErrorDismiss = { state.eventSink(ReportRoomEvents.ClearReportAction) }
|
||||||
|
)
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.screen_report_room_title),
|
||||||
|
style = ElementTheme.typography.aliasScreenTitle,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
BackButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.imePadding()
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
TextField(
|
||||||
|
value = state.reason,
|
||||||
|
onValueChange = { state.eventSink(ReportRoomEvents.UpdateReason(it)) },
|
||||||
|
placeholder = stringResource(R.string.screen_report_room_reason_placeholder),
|
||||||
|
minLines = 3,
|
||||||
|
enabled = !isReporting,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.heightIn(min = 90.dp),
|
||||||
|
supportingText = stringResource(R.string.screen_report_room_reason_footer),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
headlineContent = {
|
||||||
|
Text(text = stringResource(CommonStrings.action_leave_room))
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
state.eventSink(ReportRoomEvents.ToggleLeaveRoom)
|
||||||
|
},
|
||||||
|
trailingContent = ListItemContent.Switch(checked = state.leaveRoom)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Button(
|
||||||
|
text = stringResource(CommonStrings.action_report),
|
||||||
|
enabled = state.canReport && !isReporting,
|
||||||
|
destructive = true,
|
||||||
|
showProgress = isReporting,
|
||||||
|
onClick = {
|
||||||
|
focusManager.clearFocus(force = true)
|
||||||
|
state.eventSink(ReportRoomEvents.Report)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ReportRoomViewPreview(
|
||||||
|
@PreviewParameter(ReportRoomStateProvider::class) state: ReportRoomState
|
||||||
|
) = ElementPreview {
|
||||||
|
ReportRoomView(
|
||||||
|
state = state,
|
||||||
|
onBackClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Vaše hlášení bylo úspěšně odesláno, ale při pokusu o opuštění místnosti jsme narazili na problém. Zkuste to prosím znovu."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Nelze opustit místnost"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Nahlaste tuto místnost svému administrátorovi. Pokud jsou zprávy zašifrované, váš administrátor je nebude moci číst."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Popište důvod…"</string>
|
||||||
|
<string name="screen_report_room_title">"Nahlásit místnost"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Cyflwynwyd eich adroddiad yn llwyddiannus, ond cododd problem wrth geisio gadael yr ystafell. Ceisiwch eto."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Methu Gadael yr Ystafell"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Adroddwch yr ystafell hon i\'ch gweinyddwr. Os yw\'r negeseuon wedi\'u hamgryptio, fydd eich gweinyddwr ddim yn gallu eu darllen."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Disgrifiwch y rheswm…"</string>
|
||||||
|
<string name="screen_report_room_title">"Adrodd ar ystafell"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Ihr Bericht wurde erfolgreich übermittelt, aber beim Versuch, den Raum zu verlassen, ist ein Problem aufgetreten. Bitte versuchen Sie es erneut."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Der Chatroom kann nicht verlassen werden"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Melden Sie diesen Chatroom Ihrem Administrator. Wenn die Nachrichten verschlüsselt sind, kann Ihr Administrator sie nicht lesen."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Beschreiben Sie den Grund…"</string>
|
||||||
|
<string name="screen_report_room_title">"Chatroom melden"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Η αναφορά σας υποβλήθηκε με επιτυχία, αλλά αντιμετωπίσαμε πρόβλημα κατά την προσπάθεια αποχώρησης από το δωμάτιο. Παρακαλώ προσπαθήστε ξανά."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Δεν είναι δυνατή η αποχώρηση από το δωμάτιο"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Αναφέρετε αυτό το δωμάτιο στον διαχειριστή σας. Εάν τα μηνύματα είναι κρυπτογραφημένα, ο διαχειριστής σας δε θα μπορεί να τα διαβάσει."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Περιγράψτε τον λόγο αναφοράς…"</string>
|
||||||
|
<string name="screen_report_room_title">"Αναφορά δωματίου"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Jututoast haldajale teatamine õnnestus, kuid jututost lahkumisel tekkis viga. Palun proovi uuesti lahkuda."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Pole võimalik lahkuda jututoast"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Teata sellest jututoast süsteemi haldajale. Kui sõnumid on krüptitud, ei saa haldaja neid lugeda."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Kirjelda põhjust…"</string>
|
||||||
|
<string name="screen_report_room_title">"Teata jututoast"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Ilmoituksesi lähetettiin onnistuneesti, mutta kohtasimme ongelman yrittäessämme poistua huoneesta. Yritä uudelleen."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Huoneesta poistuminen epäonnistui"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Ilmoita tästä huoneesta palvelimesi ylläpitäjälle. Jos viestit on salattu, ylläpitäjäsi ei voi lukea niitä."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Kuvaile syytä…"</string>
|
||||||
|
<string name="screen_report_room_title">"Ilmoita huoneesta"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Votre rapport a été envoyé avec succès, mais nous avons rencontré un problème en essayant de quitter le salon. Veuillez réessayer."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Impossible de quitter le salon"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Signaler ce salon à votre admin. Si les messages sont chiffrés, votre admin ne pourra pas les lire."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Décrivez la raison…"</string>
|
||||||
|
<string name="screen_report_room_title">"Signaler le salon"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"A jelentése sikeresen el lett küldve, de hibát találtunk a szoba elhagyása során. Próbálja újra."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Nem tudja elhagyni a szobát"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"A szoba jelentése a rendszergazdának. Ha az üzenetek titkosítva vannak, akkor a rendszergazda nem fogja tudni elolvasni őket."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Írja le az okot…"</string>
|
||||||
|
<string name="screen_report_room_title">"Szoba jelentése"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Rapporten din ble sendt inn, men vi oppdaget et problem da vi prøvde å forlate rommet. Prøv igjen."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Kan ikke forlate rommet"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Rapporter dette rommet til administratoren din. Hvis meldingene er kryptert, vil administratoren ikke kunne lese dem."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Beskriv årsaken…"</string>
|
||||||
|
<string name="screen_report_room_title">"Rapporter rom"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Twoje zgłoszenie zostało wysłane pomyślnie, ale napotkaliśmy problem podczas opuszczania pokoju. Spróbuj ponownie."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Nie można wyjść z pokoju"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Zgłoś ten pokój swojemu administratorowi. Jeśli wiadomości są zaszyfrowane, administrator nie będzie mógł ich odczytać."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Opisz powód…"</string>
|
||||||
|
<string name="screen_report_room_title">"Zgłoś pokój"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Невозможно покинуть комнату"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Сообщите об этой комнате своему администратору. Если сообщения зашифрованы, ваш администратор не сможет их прочитать."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Опишите причину…"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Vaša správa bola úspešne odoslaná, ale pri pokuse o opustenie miestnosti sme narazili na problém. Skúste to prosím znova."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Nie je možné opustiť miestnosť"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Nahláste túto miestnosť svojmu správcovi. Ak sú správy zašifrované, váš správca ich nebude môcť prečítať."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Popíšte dôvod…"</string>
|
||||||
|
<string name="screen_report_room_title">"Nahlásiť miestnosť"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Din anmälan skickades in framgångsrikt, men vi stötte på ett problem när vi försökte lämna rummet. Vänligen försök igen."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Kunde inte lämna rummet"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Anmäl det här rummet till din administratör. Om meddelandena är krypterade kommer din administratör inte att kunna läsa dem."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Beskriv anledningen …"</string>
|
||||||
|
<string name="screen_report_room_title">"Rapportera rum"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Ваша скарга надіслана, але ми зіткнулися з проблемою під час спроби вийти з кімнати. Повторіть спробу."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Не вдалося вийти з кімнати"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Поскаржтеся на цю кімнату своєму адміністратору. Якщо повідомлення зашифровані, ваш адміністратор не зможе їх прочитати."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Опишіть причину…"</string>
|
||||||
|
<string name="screen_report_room_title">"Поскаржитися на кімнату"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"您的回報已成功遞交,但我們嘗試離開聊天室時遇到了問題。請再試一次。"</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"無法離開聊天室"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"將此聊天室回報給您的管理員。若訊息已加密,您的管理員將無法讀取它們。"</string>
|
||||||
|
<string name="screen_report_room_title">"回報聊天室"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
|
<string name="screen_report_room_leave_failed_alert_message">"Your report was submitted successfully, but we encountered an issue while trying to leave the room. Please try again."</string>
|
||||||
|
<string name="screen_report_room_leave_failed_alert_title">"Unable to Leave Room"</string>
|
||||||
|
<string name="screen_report_room_reason_footer">"Report this room to your admin. If the messages are encrypted, your admin will not be able to read them."</string>
|
||||||
|
<string name="screen_report_room_reason_placeholder">"Describe the reason to report…"</string>
|
||||||
|
<string name="screen_report_room_title">"Report room"</string>
|
||||||
|
</resources>
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
|
import io.element.android.libraries.matrix.test.room.FakeBaseRoom
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class DefaultReportRoomTest {
|
||||||
|
private val roomId = A_ROOM_ID
|
||||||
|
private val successLeaveRoomLambda = lambdaRecorder<Result<Unit>> { -> Result.success(Unit) }
|
||||||
|
private val successReportRoomLambda =
|
||||||
|
lambdaRecorder<String?, Result<Unit>> { _ -> Result.success(Unit) }
|
||||||
|
|
||||||
|
private val failureLeaveRoomLambda =
|
||||||
|
lambdaRecorder<Result<Unit>> { -> Result.failure(Exception("Leave room error")) }
|
||||||
|
private val failureReportRoomLambda =
|
||||||
|
lambdaRecorder<String?, Result<Unit>> { _ -> Result.failure(Exception("Report room error")) }
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=false, report=false, nothing is called`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = false)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
assert(successLeaveRoomLambda).isNeverCalled()
|
||||||
|
assert(successReportRoomLambda).isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=false, report=true, report room success`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = false)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
assert(successLeaveRoomLambda).isNeverCalled()
|
||||||
|
assert(successReportRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value("Spam"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=true, report=false, leave room success`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = false, reason = "", shouldLeave = true)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
assert(successLeaveRoomLambda).isCalledOnce()
|
||||||
|
assert(successReportRoomLambda).isNeverCalled()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=true, report=true, leave room success`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
|
||||||
|
|
||||||
|
assertThat(result.isSuccess).isTrue()
|
||||||
|
assert(successLeaveRoomLambda).isCalledOnce()
|
||||||
|
assert(successReportRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value("Spam"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=true, report=true, leave room failed`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = failureLeaveRoomLambda,
|
||||||
|
reportRoomResult = successReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.LeftRoomFailed)
|
||||||
|
assert(failureLeaveRoomLambda).isCalledOnce()
|
||||||
|
assert(successReportRoomLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `report room, leave=true, report=true, report room failed`() = runTest {
|
||||||
|
val room = FakeBaseRoom(
|
||||||
|
roomId = roomId,
|
||||||
|
leaveRoomLambda = successLeaveRoomLambda,
|
||||||
|
reportRoomResult = failureReportRoomLambda
|
||||||
|
)
|
||||||
|
val client = FakeMatrixClient().apply {
|
||||||
|
givenGetRoomResult(roomId, room)
|
||||||
|
}
|
||||||
|
val reportRoom = DefaultReportRoom(client = client)
|
||||||
|
|
||||||
|
val result = reportRoom(roomId, shouldReport = true, reason = "Spam", shouldLeave = true)
|
||||||
|
|
||||||
|
assertThat(result.isFailure).isTrue()
|
||||||
|
assertThat(result.exceptionOrNull()).isEqualTo(ReportRoom.Exception.ReportRoomFailed)
|
||||||
|
assert(successLeaveRoomLambda).isNeverCalled()
|
||||||
|
assert(failureReportRoomLambda).isCalledOnce()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.reportroom.impl
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import io.element.android.features.reportroom.impl.fakes.FakeReportRoom
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
|
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||||
|
import io.element.android.tests.testutils.lambda.any
|
||||||
|
import io.element.android.tests.testutils.lambda.assert
|
||||||
|
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||||
|
import io.element.android.tests.testutils.lambda.value
|
||||||
|
import io.element.android.tests.testutils.test
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class ReportRoomPresenterTest {
|
||||||
|
@Test
|
||||||
|
fun `present - initial state`() = runTest {
|
||||||
|
val presenter = createReportRoomPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reason).isEmpty()
|
||||||
|
assertThat(state.leaveRoom).isFalse()
|
||||||
|
assertThat(state.reportAction).isEqualTo(AsyncAction.Uninitialized)
|
||||||
|
assertThat(state.canReport).isFalse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - update form values`() = runTest {
|
||||||
|
val presenter = createReportRoomPresenter()
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reason).isEmpty()
|
||||||
|
assertThat(state.canReport).isFalse()
|
||||||
|
assertThat(state.leaveRoom).isFalse()
|
||||||
|
state.eventSink(ReportRoomEvents.UpdateReason("Spam"))
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reason).isEqualTo("Spam")
|
||||||
|
assertThat(state.canReport).isTrue()
|
||||||
|
assertThat(state.leaveRoom).isFalse()
|
||||||
|
state.eventSink(ReportRoomEvents.ToggleLeaveRoom)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.leaveRoom).isTrue()
|
||||||
|
assertThat(state.canReport).isTrue()
|
||||||
|
assertThat(state.canReport).isTrue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - report room success`() = runTest {
|
||||||
|
val roomId = A_ROOM_ID
|
||||||
|
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ -> Result.success(Unit) }
|
||||||
|
val reportRoom = FakeReportRoom(
|
||||||
|
lambda = reportRoomLambda
|
||||||
|
)
|
||||||
|
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom)
|
||||||
|
awaitItem().eventSink(ReportRoomEvents.Report)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Success::class.java)
|
||||||
|
}
|
||||||
|
assert(reportRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(roomId), value(true), any(), value(true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - report failed`() = runTest {
|
||||||
|
val roomId = A_ROOM_ID
|
||||||
|
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ ->
|
||||||
|
Result.failure(ReportRoom.Exception.ReportRoomFailed)
|
||||||
|
}
|
||||||
|
val reportRoom = FakeReportRoom(
|
||||||
|
lambda = reportRoomLambda
|
||||||
|
)
|
||||||
|
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().eventSink(ReportRoomEvents.Report)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
assert(reportRoomLambda)
|
||||||
|
.isCalledOnce()
|
||||||
|
.with(value(roomId), value(true), any(), any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `present - leave room failed after report room success`() = runTest {
|
||||||
|
val roomId = A_ROOM_ID
|
||||||
|
val reportRoomLambda = lambdaRecorder<RoomId, Boolean, String, Boolean, Result<Unit>> { _, _, _, _ ->
|
||||||
|
Result.failure(ReportRoom.Exception.LeftRoomFailed)
|
||||||
|
}
|
||||||
|
val reportRoom = FakeReportRoom(
|
||||||
|
lambda = reportRoomLambda
|
||||||
|
)
|
||||||
|
val presenter = createReportRoomPresenter(roomId = roomId, reportRoom = reportRoom)
|
||||||
|
presenter.test {
|
||||||
|
awaitItem().eventSink(ReportRoomEvents.ToggleLeaveRoom)
|
||||||
|
awaitItem().eventSink(ReportRoomEvents.Report)
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
state.eventSink(ReportRoomEvents.Report)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Loading::class.java)
|
||||||
|
}
|
||||||
|
awaitItem().also { state ->
|
||||||
|
assertThat(state.reportAction).isInstanceOf(AsyncAction.Failure::class.java)
|
||||||
|
}
|
||||||
|
assert(reportRoomLambda)
|
||||||
|
.isCalledExactly(2)
|
||||||
|
.withSequence(
|
||||||
|
// The first call should report the room and try leaving it
|
||||||
|
listOf(value(roomId), value(true), any(), value(true)),
|
||||||
|
// The second call should not report the room again
|
||||||
|
listOf(value(roomId), value(false), any(), value(true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createReportRoomPresenter(
|
||||||
|
roomId: RoomId = A_ROOM_ID,
|
||||||
|
reportRoom: ReportRoom = FakeReportRoom()
|
||||||
|
): ReportRoomPresenter {
|
||||||
|
return ReportRoomPresenter(roomId, reportRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue