diff --git a/changelog.d/8998.misc b/changelog.d/8998.misc
new file mode 100644
index 00000000000..f6744ff12fe
--- /dev/null
+++ b/changelog.d/8998.misc
@@ -0,0 +1 @@
+Add action to report room.
diff --git a/library/ui-strings/src/main/res/values/strings.xml b/library/ui-strings/src/main/res/values/strings.xml
index 54d783fc85d..803510ed766 100644
--- a/library/ui-strings/src/main/res/values/strings.xml
+++ b/library/ui-strings/src/main/res/values/strings.xml
@@ -2369,6 +2369,8 @@
Poll history
Uploads
+ Report Room
+ The room has been successfully reported.
Leave Room
Leave
"Leaving the room…"
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt
index a444e2346e6..c3ee5d75c1e 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/reporting/ReportingService.kt
@@ -26,4 +26,9 @@ interface ReportingService {
* Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid
*/
suspend fun reportContent(eventId: String, score: Int, reason: String)
+
+ /**
+ * Report a room.
+ */
+ suspend fun reportRoom(reason: String)
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
index aa4bdb1dd4f..33fa1d8ce07 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomAPI.kt
@@ -36,6 +36,7 @@ import org.matrix.android.sdk.internal.session.room.read.ReadBody
import org.matrix.android.sdk.internal.session.room.relation.RelationsResponse
import org.matrix.android.sdk.internal.session.room.relation.threads.ThreadSummariesResponse
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentBody
+import org.matrix.android.sdk.internal.session.room.reporting.ReportRoomBody
import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.session.room.send.model.EventRedactBody
import org.matrix.android.sdk.internal.session.room.tags.TagBody
@@ -375,6 +376,18 @@ internal interface RoomAPI {
@Body body: ReportContentBody,
)
+ /**
+ * Reports a room as inappropriate to the server, which may then notify the appropriate people.
+ *
+ * @param roomId the room id
+ * @param body body containing the reason
+ */
+ @POST(NetworkConstants.URI_API_PREFIX_PATH_V3 + "rooms/{roomId}/report")
+ suspend fun reportRoom(
+ @Path("roomId") roomId: String,
+ @Body body: ReportRoomBody,
+ )
+
/**
* Get a list of aliases maintained by the local server for the given room.
* Ref: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-rooms-roomid-aliases
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
index 673a9796334..d14568ebc31 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt
@@ -114,7 +114,9 @@ import org.matrix.android.sdk.internal.session.room.relation.threads.DefaultFetc
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadSummariesTask
import org.matrix.android.sdk.internal.session.room.relation.threads.FetchThreadTimelineTask
import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportContentTask
+import org.matrix.android.sdk.internal.session.room.reporting.DefaultReportRoomTask
import org.matrix.android.sdk.internal.session.room.reporting.ReportContentTask
+import org.matrix.android.sdk.internal.session.room.reporting.ReportRoomTask
import org.matrix.android.sdk.internal.session.room.state.DefaultSendStateTask
import org.matrix.android.sdk.internal.session.room.state.SendStateTask
import org.matrix.android.sdk.internal.session.room.tags.AddTagToRoomTask
@@ -281,6 +283,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindReportContentTask(task: DefaultReportContentTask): ReportContentTask
+ @Binds
+ abstract fun bindReportRoomTask(task: DefaultReportRoomTask): ReportRoomTask
+
@Binds
abstract fun bindGetContextOfEventTask(task: DefaultGetContextOfEventTask): GetContextOfEventTask
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt
index 1e32bf5e16f..ba8a6cb4531 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/DefaultReportingService.kt
@@ -23,7 +23,8 @@ import org.matrix.android.sdk.api.session.room.reporting.ReportingService
internal class DefaultReportingService @AssistedInject constructor(
@Assisted private val roomId: String,
- private val reportContentTask: ReportContentTask
+ private val reportContentTask: ReportContentTask,
+ private val reportRoomTask: ReportRoomTask,
) : ReportingService {
@AssistedFactory
@@ -35,4 +36,9 @@ internal class DefaultReportingService @AssistedInject constructor(
val params = ReportContentTask.Params(roomId, eventId, score, reason)
reportContentTask.execute(params)
}
+
+ override suspend fun reportRoom(reason: String) {
+ val params = ReportRoomTask.Params(roomId, reason)
+ reportRoomTask.execute(params)
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomBody.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomBody.kt
new file mode 100644
index 00000000000..8e211320635
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomBody.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2025 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.reporting
+
+import com.squareup.moshi.Json
+import com.squareup.moshi.JsonClass
+
+@JsonClass(generateAdapter = true)
+internal data class ReportRoomBody(
+ /**
+ * Required. The reason the content is being reported. May be blank.
+ */
+ @Json(name = "reason") val reason: String
+)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomTask.kt
new file mode 100644
index 00000000000..54b0efd207e
--- /dev/null
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/reporting/ReportRoomTask.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2025 The Matrix.org Foundation C.I.C.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.matrix.android.sdk.internal.session.room.reporting
+
+import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
+import org.matrix.android.sdk.internal.network.executeRequest
+import org.matrix.android.sdk.internal.session.room.RoomAPI
+import org.matrix.android.sdk.internal.task.Task
+import javax.inject.Inject
+
+internal interface ReportRoomTask : Task {
+ data class Params(
+ val roomId: String,
+ val reason: String,
+ )
+}
+
+internal class DefaultReportRoomTask @Inject constructor(
+ private val roomAPI: RoomAPI,
+ private val globalErrorReceiver: GlobalErrorReceiver
+) : ReportRoomTask {
+
+ override suspend fun execute(params: ReportRoomTask.Params) {
+ return executeRequest(globalErrorReceiver) {
+ roomAPI.reportRoom(params.roomId, ReportRoomBody(params.reason))
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
index 6afe5f8aa72..a11d8afbfcd 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileAction.kt
@@ -18,4 +18,5 @@ sealed class RoomProfileAction : VectorViewModelAction {
object CreateShortcut : RoomProfileAction()
object RestoreEncryptionState : RoomProfileAction()
data class SetEncryptToVerifiedDeviceOnly(val enabled: Boolean) : RoomProfileAction()
+ data class ReportRoom(val reason: String) : RoomProfileAction()
}
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
index 0ce243e8101..7dd11c281c1 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileController.kt
@@ -52,6 +52,7 @@ class RoomProfileController @Inject constructor(
fun onUploadsClicked()
fun createShortcut()
fun onSettingsClicked()
+ fun onReportRoomClicked()
fun onLeaveRoomClicked()
fun onRoomAliasesClicked()
fun onRoomPermissionsClicked()
@@ -279,6 +280,13 @@ class RoomProfileController @Inject constructor(
action = { callback?.createShortcut() }
)
}
+ buildProfileAction(
+ id = "Report",
+ title = stringProvider.getString(CommonStrings.room_profile_section_more_report),
+ icon = R.drawable.ic_report_spam,
+ editable = false,
+ action = { callback?.onReportRoomClicked() }
+ )
buildProfileAction(
id = "leave",
title = stringProvider.getString(
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
index 13f02d9f059..1a56de9fab6 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileFragment.kt
@@ -33,6 +33,7 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.utils.copyToClipboard
import im.vector.app.core.utils.startSharePlainTextIntent
+import im.vector.app.databinding.DialogReportContentBinding
import im.vector.app.databinding.FragmentMatrixProfileBinding
import im.vector.app.databinding.ViewStubRoomProfileHeaderBinding
import im.vector.app.features.analytics.plan.Interaction
@@ -123,6 +124,7 @@ class RoomProfileFragment :
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
RoomProfileViewEvents.DismissLoading -> dismissLoadingDialog()
+ is RoomProfileViewEvents.Success -> dismissSuccessDialog(it.message)
}
}
roomListQuickActionsSharedActionViewModel
@@ -133,6 +135,17 @@ class RoomProfileFragment :
setupLongClicks()
}
+ private fun dismissSuccessDialog(message: CharSequence) {
+ MaterialAlertDialogBuilder(
+ requireActivity(),
+ im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_NegativeDestructive
+ )
+ .setTitle(CommonStrings.room_profile_section_more_report)
+ .setMessage(message)
+ .setPositiveButton(CommonStrings.ok, null)
+ .show()
+ }
+
private fun setupWaitingView() {
views.waitingView.waitingStatusText.setText(CommonStrings.please_wait)
views.waitingView.waitingStatusText.isVisible = true
@@ -286,6 +299,26 @@ class RoomProfileFragment :
ShortcutManagerCompat.requestPinShortcut(requireContext(), onShortcutReady.shortcutInfo, null)
}
+ override fun onReportRoomClicked() {
+ promptReasonToReportRoom()
+ }
+
+ private fun promptReasonToReportRoom() {
+ val inflater = requireActivity().layoutInflater
+ val layout = inflater.inflate(R.layout.dialog_report_content, null)
+ val views = DialogReportContentBinding.bind(layout)
+
+ MaterialAlertDialogBuilder(requireActivity())
+ .setTitle(CommonStrings.room_profile_section_more_report)
+ .setView(layout)
+ .setPositiveButton(CommonStrings.report_content_custom_submit) { _, _ ->
+ val reason = views.dialogReportContentInput.text.toString()
+ roomProfileViewModel.handle(RoomProfileAction.ReportRoom(reason))
+ }
+ .setNegativeButton(CommonStrings.action_cancel, null)
+ .show()
+ }
+
override fun onLeaveRoomClicked() {
val isPublicRoom = roomProfileViewModel.isPublicRoom()
val message = buildString {
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
index 12a0b327d26..1b120856636 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewEvents.kt
@@ -17,6 +17,7 @@ sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
object DismissLoading : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
+ data class Success(val message: CharSequence) : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()
data class OnShortcutReady(val shortcutInfo: ShortcutInfoCompat) : RoomProfileViewEvents()
diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
index 7e18b327f1d..83b1c5a9503 100644
--- a/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/roomprofile/RoomProfileViewModel.kt
@@ -176,6 +176,26 @@ class RoomProfileViewModel @AssistedInject constructor(
RoomProfileAction.CreateShortcut -> handleCreateShortcut()
RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState()
is RoomProfileAction.SetEncryptToVerifiedDeviceOnly -> setEncryptToVerifiedDeviceOnly(action.enabled)
+ is RoomProfileAction.ReportRoom -> handleReportRoom(action.reason)
+ }
+ }
+
+ private fun handleReportRoom(reason: String) {
+ _viewEvents.post(RoomProfileViewEvents.Loading())
+ session.coroutineScope.launch {
+ try {
+ room.reportingService().reportRoom(reason = reason)
+ _viewEvents.post(
+ RoomProfileViewEvents.Success(
+ stringProvider.getString(CommonStrings.room_profile_section_more_report_success_content)
+ )
+ )
+ } catch (failure: Throwable) {
+ Timber.e(failure, "Failed to report room ${room.roomId}")
+ _viewEvents.post(RoomProfileViewEvents.Failure(failure))
+ } finally {
+ _viewEvents.post(RoomProfileViewEvents.DismissLoading)
+ }
}
}