diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index 9f91e5b276..70c9c6e36c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.room.crypto.RoomCryptoService import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService @@ -38,6 +39,7 @@ interface Room : ReadService, MembershipService, StateService, + ReportingService, RelationService, RoomCryptoService { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt new file mode 100644 index 0000000000..9dbc6d0e9e --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/reporting/ReportingService.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 im.vector.matrix.android.api.session.room.reporting + +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable + +/** + * This interface defines methods to report content of an event. + */ +interface ReportingService { + + /** + * Report content + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-rooms-roomid-report-eventid + */ + fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 7d957ccdad..fea827fd25 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.members.MembershipService import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.relation.RelationService +import im.vector.matrix.android.api.session.room.reporting.ReportingService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.DraftService import im.vector.matrix.android.api.session.room.send.SendService @@ -44,18 +45,20 @@ internal class DefaultRoom @Inject constructor(override val roomId: String, private val sendService: SendService, private val draftService: DraftService, private val stateService: StateService, + private val reportingService: ReportingService, private val readService: ReadService, private val cryptoService: CryptoService, private val relationService: RelationService, - private val roomMembersService: MembershipService -) : Room, - TimelineService by timelineService, - SendService by sendService, - DraftService by draftService, - StateService by stateService, - ReadService by readService, - RelationService by relationService, - MembershipService by roomMembersService { + private val roomMembersService: MembershipService) : + Room, + TimelineService by timelineService, + SendService by sendService, + DraftService by draftService, + StateService by stateService, + ReportingService by reportingService, + ReadService by readService, + RelationService by relationService, + MembershipService by roomMembersService { override fun getRoomSummaryLive(): LiveData> { val liveData = monarchy.findAllMappedWithChanges( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index daee1c914c..797dbed31c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -27,6 +27,7 @@ import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.session.room.membership.RoomMembersResponse import im.vector.matrix.android.internal.session.room.membership.joining.InviteBody import im.vector.matrix.android.internal.session.room.relation.RelationsResponse +import im.vector.matrix.android.internal.session.room.reporting.ReportContentBody import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse @@ -245,4 +246,16 @@ internal interface RoomAPI { @Path("eventId") parent_id: String, @Body reason: Map ): Call + + /** + * Reports an event as inappropriate to the server, which may then notify the appropriate people. + * + * @param roomId the room id + * @param eventId the event to report content + * @param body body containing score and reason + */ + @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/report/{eventId}") + fun reportContent(@Path("roomId") roomId: String, + @Path("eventId") eventId: String, + @Body body: ReportContentBody): Call } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 65a3624d2c..e2199782f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -24,6 +24,7 @@ import im.vector.matrix.android.internal.session.room.draft.DefaultDraftService import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService import im.vector.matrix.android.internal.session.room.read.DefaultReadService import im.vector.matrix.android.internal.session.room.relation.DefaultRelationService +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportingService import im.vector.matrix.android.internal.session.room.send.DefaultSendService import im.vector.matrix.android.internal.session.room.state.DefaultStateService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService @@ -40,6 +41,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona private val sendServiceFactory: DefaultSendService.Factory, private val draftServiceFactory: DefaultDraftService.Factory, private val stateServiceFactory: DefaultStateService.Factory, + private val reportingServiceFactory: DefaultReportingService.Factory, private val readServiceFactory: DefaultReadService.Factory, private val relationServiceFactory: DefaultRelationService.Factory, private val membershipServiceFactory: DefaultMembershipService.Factory) : @@ -54,6 +56,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona sendServiceFactory.create(roomId), draftServiceFactory.create(roomId), stateServiceFactory.create(roomId), + reportingServiceFactory.create(roomId), readServiceFactory.create(roomId), cryptoService, relationServiceFactory.create(roomId), diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index 5755d6b46e..1aca492b94 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -45,6 +45,8 @@ import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkers import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.relation.* +import im.vector.matrix.android.internal.session.room.reporting.DefaultReportContentTask +import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask import im.vector.matrix.android.internal.session.room.state.SendStateTask import im.vector.matrix.android.internal.session.room.timeline.* @@ -114,6 +116,9 @@ internal abstract class RoomModule { @Binds abstract fun bindSendStateTask(sendStateTask: DefaultSendStateTask): SendStateTask + @Binds + abstract fun bindReportContentTask(reportContentTask: DefaultReportContentTask): ReportContentTask + @Binds abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt new file mode 100644 index 0000000000..3b64cce439 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/DefaultReportingService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.session.room.reporting + +import com.squareup.inject.assisted.Assisted +import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.reporting.ReportingService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith + +internal class DefaultReportingService @AssistedInject constructor(@Assisted private val roomId: String, + private val taskExecutor: TaskExecutor, + private val reportContentTask: ReportContentTask +) : ReportingService { + + @AssistedInject.Factory + interface Factory { + fun create(roomId: String): ReportingService + } + + override fun reportContent(eventId: String, score: Int, reason: String, callback: MatrixCallback): Cancelable { + val params = ReportContentTask.Params(roomId, eventId, score, reason) + + return reportContentTask + .configureWith(params) { + this.callback = callback + } + .executeBy(taskExecutor) + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt new file mode 100644 index 0000000000..2cf33551ce --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentBody.kt @@ -0,0 +1,33 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.session.room.reporting + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class ReportContentBody( + /** + * Required. The score to rate this content as where -100 is most offensive and 0 is inoffensive. + */ + @Json(name = "score") val score: Int, + + /** + * 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/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt new file mode 100644 index 0000000000..60c031158a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/reporting/ReportContentTask.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2019 New Vector Ltd + * + * 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 im.vector.matrix.android.internal.session.room.reporting + +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.task.Task +import javax.inject.Inject + +internal interface ReportContentTask : Task { + data class Params( + val roomId: String, + val eventId: String, + val score: Int, + val reason: String + ) +} + +internal class DefaultReportContentTask @Inject constructor(private val roomAPI: RoomAPI) : ReportContentTask { + override suspend fun execute(params: ReportContentTask.Params) { + return executeRequest { + apiCall = roomAPI.reportContent(params.roomId, params.eventId, ReportContentBody(params.score, params.reason)) + } + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt index d032182994..eec9eef5dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailActions.kt @@ -49,6 +49,9 @@ sealed class RoomDetailActions { data class ResendMessage(val eventId: String) : RoomDetailActions() data class RemoveFailedEcho(val eventId: String) : RoomDetailActions() + + data class ReportContent(val eventId: String, val reason: String) : RoomDetailActions() + object ClearSendQueue : RoomDetailActions() object ResendAll : RoomDetailActions() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 7447bf92e6..d26dba44e1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -93,7 +93,9 @@ import im.vector.riotx.features.home.room.detail.composer.TextComposerViewModel import im.vector.riotx.features.home.room.detail.composer.TextComposerViewState import im.vector.riotx.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController -import im.vector.riotx.features.home.room.detail.timeline.action.* +import im.vector.riotx.features.home.room.detail.timeline.action.ActionsHandler +import im.vector.riotx.features.home.room.detail.timeline.action.MessageActionsBottomSheet +import im.vector.riotx.features.home.room.detail.timeline.action.SimpleAction import im.vector.riotx.features.home.room.detail.timeline.edithistory.ViewEditHistoryBottomSheet import im.vector.riotx.features.home.room.detail.timeline.item.* import im.vector.riotx.features.home.room.detail.timeline.reactions.ViewReactionsBottomSheet @@ -968,23 +970,23 @@ class RoomDetailFragment : private fun handleActions(action: SimpleAction) { when (action) { - is SimpleAction.AddReaction -> { + is SimpleAction.AddReaction -> { startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) } - is SimpleAction.ViewReactions -> { + is SimpleAction.ViewReactions -> { ViewReactionsBottomSheet.newInstance(roomDetailArgs.roomId, action.messageInformationData) .show(requireActivity().supportFragmentManager, "DISPLAY_REACTIONS") } - is SimpleAction.Copy -> { + is SimpleAction.Copy -> { // I need info about the current selected message :/ copyToClipboard(requireContext(), action.content, false) val msg = requireContext().getString(R.string.copied_to_clipboard) showSnackWithMessage(msg, Snackbar.LENGTH_SHORT) } - is SimpleAction.Delete -> { + is SimpleAction.Delete -> { roomDetailViewModel.process(RoomDetailActions.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) } - is SimpleAction.Share -> { + is SimpleAction.Share -> { // TODO current data communication is too limited // Need to now the media type // TODO bad, just POC @@ -1012,10 +1014,10 @@ class RoomDetailFragment : } ) } - is SimpleAction.ViewEditHistory -> { + is SimpleAction.ViewEditHistory -> { onEditedDecorationClicked(action.messageInformationData) } - is SimpleAction.ViewSource -> { + is SimpleAction.ViewSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1026,7 +1028,7 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.ViewDecryptedSource -> { + is SimpleAction.ViewDecryptedSource -> { val view = LayoutInflater.from(requireContext()).inflate(R.layout.dialog_event_content, null) view.findViewById(R.id.event_content_text_view)?.let { it.text = action.content @@ -1037,31 +1039,38 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok, null) .show() } - is SimpleAction.QuickReact -> { + is SimpleAction.QuickReact -> { // eventId,ClickedOn,Add roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(action.eventId, action.clickedOn, action.add)) } - is SimpleAction.Edit -> { + is SimpleAction.Edit -> { roomDetailViewModel.process(RoomDetailActions.EnterEditMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.Quote -> { + is SimpleAction.Quote -> { roomDetailViewModel.process(RoomDetailActions.EnterQuoteMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.Reply -> { + is SimpleAction.Reply -> { roomDetailViewModel.process(RoomDetailActions.EnterReplyMode(action.eventId, composerLayout.composerEditText.text.toString())) } - is SimpleAction.CopyPermalink -> { + is SimpleAction.CopyPermalink -> { val permalink = PermalinkFactory.createPermalink(roomDetailArgs.roomId, action.eventId) copyToClipboard(requireContext(), permalink, false) showSnackWithMessage(requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - is SimpleAction.Resend -> { + is SimpleAction.Resend -> { roomDetailViewModel.process(RoomDetailActions.ResendMessage(action.eventId)) } - is SimpleAction.Remove -> { + is SimpleAction.Remove -> { roomDetailViewModel.process(RoomDetailActions.RemoveFailedEcho(action.eventId)) } - else -> { + is SimpleAction.ReportContentSpam -> { + roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is spam")) + } + is SimpleAction.ReportContentInappropriate -> { + roomDetailViewModel.process(RoomDetailActions.ReportContent(action.eventId, "This message is inappropriate")) + } + // TODO Custom + else -> { Toast.makeText(context, "Action $action is not implemented yet", Toast.LENGTH_LONG).show() } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 57a2c76f61..ee60a89bc1 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -21,10 +21,7 @@ import android.text.TextUtils import androidx.annotation.IdRes import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import com.airbnb.mvrx.FragmentViewModelContext -import com.airbnb.mvrx.MvRxViewModelFactory -import com.airbnb.mvrx.Success -import com.airbnb.mvrx.ViewModelContext +import com.airbnb.mvrx.* import com.jakewharton.rxrelay2.BehaviorRelay import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject @@ -155,6 +152,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailActions.ResendAll -> handleResendAll() is RoomDetailActions.SetReadMarkerAction -> handleSetReadMarkerAction(action) is RoomDetailActions.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailActions.ReportContent -> handleReportContent(action) } } @@ -708,6 +706,32 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro room.markAllAsRead(object : MatrixCallback {}) } + private fun handleReportContent(action: RoomDetailActions.ReportContent) { + setState { + copy( + reportContentRequest = Loading() + ) + } + + room.reportContent(action.eventId, -100, action.reason, object : MatrixCallback { + override fun onSuccess(data: Unit) { + setState { + copy( + reportContentRequest = Success(Unit) + ) + } + } + + override fun onFailure(failure: Throwable) { + setState { + copy( + reportContentRequest = Fail(failure) + ) + } + } + }) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 03110858a1..ba20b9ed3f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -52,7 +52,8 @@ data class RoomDetailViewState( val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async = Uninitialized, val syncState: SyncState = SyncState.IDLE, - val highlightedEventId: String? = null + val highlightedEventId: String? = null, + val reportContentRequest: Async = Uninitialized ) : MvRxState { constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)