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:
ganfra 2025-05-02 12:25:19 +02:00 committed by GitHub
parent db6bc7c04f
commit c2568f84d2
229 changed files with 3995 additions and 1210 deletions

View file

@ -28,6 +28,7 @@ import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
import io.element.android.features.messages.api.MessagesEntryPoint
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
import io.element.android.features.reportroom.api.ReportRoomEntryPoint
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
import io.element.android.features.roomdetails.impl.edit.RoomDetailsEditNode
import io.element.android.features.roomdetails.impl.invite.RoomInviteMembersNode
@ -71,6 +72,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
private val mediaGalleryEntryPoint: MediaGalleryEntryPoint,
private val verifySessionEntryPoint: VerifySessionEntryPoint,
private val reportRoomEntryPoint: ReportRoomEntryPoint,
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<RoomDetailsEntryPoint.Params>().first().initialElement.toNavTarget(),
@ -127,6 +129,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
@Parcelize
data class VerifyUser(val userId: UserId) : NavTarget
@Parcelize
data object ReportRoom : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -189,6 +194,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton)
elementCallEntryPoint.startCall(inputs)
}
override fun openReportRoom() {
backstack.push(NavTarget.ReportRoom)
}
}
createNode<RoomDetailsNode>(buildContext, listOf(roomDetailsCallback))
}
@ -340,6 +349,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
})
.build()
}
is NavTarget.ReportRoom -> {
reportRoomEntryPoint.createNode(this, buildContext, room.roomId)
}
}
}

View file

@ -53,6 +53,7 @@ class RoomDetailsNode @AssistedInject constructor(
fun openSecurityAndPrivacy()
fun openDmUserProfile(userId: UserId)
fun onJoinCall()
fun openReportRoom()
}
private val callbacks = plugins<Callback>()
@ -132,6 +133,10 @@ class RoomDetailsNode @AssistedInject constructor(
callbacks.forEach { it.openDmUserProfile(userId) }
}
private fun onReportRoomClick() {
callbacks.forEach { it.openReportRoom() }
}
@Composable
override fun View(modifier: Modifier) {
val context = LocalContext.current
@ -166,6 +171,7 @@ class RoomDetailsNode @AssistedInject constructor(
onKnockRequestsClick = ::openKnockRequestsLists,
onSecurityAndPrivacyClick = ::openSecurityAndPrivacy,
onProfileClick = ::onProfileClick,
onReportRoomClick = ::onReportRoomClick,
)
}
}

View file

@ -16,8 +16,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import im.vector.app.features.analytics.plan.Interaction
import io.element.android.appconfig.MatrixConfiguration
import io.element.android.features.leaveroom.api.LeaveRoomEvent
import io.element.android.features.leaveroom.api.LeaveRoomState
import io.element.android.features.messages.api.pinned.IsPinnedMessagesFeatureEnabled
@ -88,12 +88,12 @@ class RoomDetailsPresenter @Inject constructor(
val joinRule by remember { derivedStateOf { roomInfo.joinRule } }
val canShowPinnedMessages = isPinnedMessagesFeatureEnabled()
var canShowMediaGallery by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
canShowMediaGallery = featureFlagService.isFeatureEnabled(FeatureFlags.MediaGallery)
}
val pinnedMessagesCount by remember { derivedStateOf { roomInfo.pinnedEventIds.size } }
val canShowMediaGallery by remember {
featureFlagService.isFeatureEnabledFlow(FeatureFlags.MediaGallery)
}.collectAsState(false)
LaunchedEffect(Unit) {
canShowNotificationSettings.value = featureFlagService.isFeatureEnabled(FeatureFlags.NotificationSettings)
if (canShowNotificationSettings.value) {
@ -208,6 +208,7 @@ class RoomDetailsPresenter @Inject constructor(
knockRequestsCount = knockRequestsCount,
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
hasMemberVerificationViolations = hasMemberVerificationViolations,
canReportRoom = MatrixConfiguration.CAN_REPORT_ROOM,
eventSink = ::handleEvents,
)
}

View file

@ -48,6 +48,7 @@ data class RoomDetailsState(
val knockRequestsCount: Int?,
val canShowSecurityAndPrivacy: Boolean,
val hasMemberVerificationViolations: Boolean,
val canReportRoom: Boolean,
val eventSink: (RoomDetailsEvent) -> Unit
) {
val roomBadges = buildList {

View file

@ -117,6 +117,7 @@ fun aRoomDetailsState(
knockRequestsCount: Int? = null,
canShowSecurityAndPrivacy: Boolean = true,
hasMemberVerificationViolations: Boolean = false,
canReportRoom: Boolean = true,
eventSink: (RoomDetailsEvent) -> Unit = {},
) = RoomDetailsState(
roomId = roomId,
@ -146,6 +147,7 @@ fun aRoomDetailsState(
knockRequestsCount = knockRequestsCount,
canShowSecurityAndPrivacy = canShowSecurityAndPrivacy,
hasMemberVerificationViolations = hasMemberVerificationViolations,
canReportRoom = canReportRoom,
eventSink = eventSink,
)

View file

@ -107,6 +107,7 @@ fun RoomDetailsView(
onKnockRequestsClick: () -> Unit,
onSecurityAndPrivacyClick: () -> Unit,
onProfileClick: (UserId) -> Unit,
onReportRoomClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val snackbarHostState = rememberSnackbarHostState(snackbarMessage = state.snackbarMessage)
@ -255,8 +256,9 @@ fun RoomDetailsView(
}
OtherActionsSection(
isDm = state.roomType is RoomDetailsType.Dm,
onLeaveRoom = { state.eventSink(RoomDetailsEvent.LeaveRoom) }
canReportRoom = state.canReportRoom,
onReportRoomClick = onReportRoomClick,
onLeaveRoomClick = { state.eventSink(RoomDetailsEvent.LeaveRoom) }
)
}
}
@ -669,22 +671,29 @@ private fun MediaGalleryItem(
}
@Composable
private fun OtherActionsSection(isDm: Boolean, onLeaveRoom: () -> Unit) {
private fun OtherActionsSection(
canReportRoom: Boolean,
onReportRoomClick: () -> Unit,
onLeaveRoomClick: () -> Unit,
) {
PreferenceCategory(showTopDivider = true) {
if (canReportRoom) {
ListItem(
headlineContent = {
Text(stringResource(CommonStrings.action_report_room))
},
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.ChatProblem())),
style = ListItemStyle.Destructive,
onClick = onReportRoomClick,
)
}
ListItem(
headlineContent = {
val leaveText = stringResource(
id = if (isDm) {
R.string.screen_room_details_leave_conversation_title
} else {
R.string.screen_room_details_leave_room_title
}
)
Text(leaveText)
Text(stringResource(CommonStrings.action_leave_room))
},
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Leave())),
style = ListItemStyle.Destructive,
onClick = onLeaveRoom,
onClick = onLeaveRoomClick,
)
}
}
@ -719,5 +728,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
onKnockRequestsClick = {},
onSecurityAndPrivacyClick = {},
onProfileClick = {},
onReportRoomClick = {},
)
}

View file

@ -283,6 +283,20 @@ class RoomDetailsViewTest {
eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom)
}
@Config(qualifiers = "h1500dp")
@Test
fun `click on report room invokes expected callback`() {
ensureCalledOnce { callback ->
rule.setRoomDetailView(
state = aRoomDetailsState(
eventSink = EventsRecorder(expectEvents = false),
),
onReportRoomClick = callback,
)
rule.clickOn(CommonStrings.action_report_room)
}
}
@Config(qualifiers = "h1024dp")
@Test
fun `click on knock requests invokes expected callback`() {
@ -333,6 +347,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
onSecurityAndPrivacyClick: () -> Unit = EnsureNeverCalled(),
onProfileClick: (UserId) -> Unit = EnsureNeverCalledWithParam(),
onReportRoomClick: () -> Unit = EnsureNeverCalled(),
) {
setContent {
RoomDetailsView(
@ -352,6 +367,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
onKnockRequestsClick = onKnockRequestsClick,
onSecurityAndPrivacyClick = onSecurityAndPrivacyClick,
onProfileClick = onProfileClick,
onReportRoomClick = onReportRoomClick,
)
}
}