diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt index ba1095f5af..7390bc4e4c 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/session/room/timeline/TimelineTest.kt @@ -18,10 +18,12 @@ package im.vector.matrix.android.session.room.timeline import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.room.timeline.Timeline import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.internal.session.room.EventRelationExtractor -import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor +import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater +import im.vector.matrix.android.internal.session.room.members.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor @@ -55,7 +57,8 @@ internal class TimelineTest : InstrumentedTest { private fun createTimeline(initialEventId: String? = null): Timeline { val taskExecutor = TaskExecutor(testCoroutineDispatchers) - val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy) + val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null)) + val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau) val paginationTask = FakePaginationTask(tokenChunkEventPersistor) val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor) val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt index 004495b57d..4a9547e3f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/UnsignedData.kt @@ -25,5 +25,5 @@ data class UnsignedData( @Json(name = "redacted_because") val redactedEvent: Event? = null, @Json(name = "transaction_id") val transactionId: String? = null, @Json(name = "prev_content") val prevContent: Map? = null, - @Json(name = "m.relations") val relations: AggregatedRelations? + @Json(name = "m.relations") val relations: AggregatedRelations? = null ) \ No newline at end of file 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 2061a29647..ae890eafad 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 @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData 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.annotation.ReactionService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -27,7 +28,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineService /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, SendService, ReadService, MembershipService, StateService { +interface Room : TimelineService, SendService, ReadService, MembershipService, StateService , ReactionService{ /** * The roomId of this room diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt new file mode 100644 index 0000000000..ace61159af --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt @@ -0,0 +1,52 @@ +/* + * 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.model.annotation + +import im.vector.matrix.android.api.util.Cancelable + +interface ReactionService { + + + /** + * Sends a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + */ + fun sendReaction(reaction: String, targetEventId: String): Cancelable + + + /** + * Undo a reaction (emoji) to the targetedEvent. + * @param reaction the reaction (preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable + + + /** + * Update a quick reaction (toggle). + * If you have reacted with agree and then you click on disagree, this call will delete(redact) + * the disagree and add the agree + * If you click on a reaction that you already reacted with, it will undo it + * @param reaction the reaction (preferably emoji) + * @param oppositeReaction the opposite reaction(preferably emoji) + * @param targetEventId the id of the event being reacted + * @param myUserId used to know if a reaction event was made by the user + */ + fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index 7cd2dbf099..6852931cbb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.session.content.ContentAttachmentData +import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.util.Cancelable @@ -48,7 +49,6 @@ interface SendService { */ fun sendMedias(attachments: List): Cancelable - - fun sendReaction(reaction: String, targetEventId: String) : Cancelable + fun redactEvent(event: Event, reason: String?): Cancelable } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index b9889b73a7..6640cb2af1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -154,7 +154,7 @@ internal class SessionModule(private val sessionParams: SessionParams) { scope(DefaultSession.SCOPE) { val groupSummaryUpdater = GroupSummaryUpdater(get()) - val eventsPruner = EventsPruner(get()) + val eventsPruner = EventsPruner(get(), get(), get(), get()) val userEntityUpdater = UserEntityUpdater(get(), get(), get()) listOf(groupSummaryUpdater, eventsPruner, userEntityUpdater) } 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 c767e5c699..80d89659a8 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 @@ -22,6 +22,7 @@ import com.zhuinden.monarchy.Monarchy 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.annotation.ReactionService import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.state.StateService @@ -39,12 +40,14 @@ internal class DefaultRoom( private val sendService: SendService, private val stateService: StateService, private val readService: ReadService, + private val reactionService: ReactionService, private val roomMembersService: MembershipService ) : Room, - TimelineService by timelineService, - SendService by sendService, - StateService by stateService, - ReadService by readService, + TimelineService by timelineService, + SendService by sendService, + StateService by stateService, + ReadService by readService, + ReactionService by reactionService, MembershipService by roomMembersService { override val roomSummary: LiveData by lazy { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt index 0df08914d9..85e7245dd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationUpdater.kt @@ -67,13 +67,14 @@ internal class EventRelationsAggregationUpdater(private val credentials: Credent sum.key = reaction sum.firstTimestamp = event.originServerTs ?: 0 sum.count = 1 + sum.sourceEvents.add(event.eventId) sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) eventSummary.reactionsSummary.add(sum) } else { //is this a known event (is possible? pagination?) if (!sum.sourceEvents.contains(eventId)) { sum.count += 1 - sum.sourceEvents.add(eventId) + sum.sourceEvents.add(event.eventId) sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender) } } 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 23e74c7456..39d9c4d38b 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 @@ -197,4 +197,22 @@ internal interface RoomAPI { @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/leave") fun leave(@Path("roomId") roomId: String, @Body params: Map): Call + + /** + * Strips all information out of an event which isn't critical to the integrity of the server-side representation of the room. + * This cannot be undone. + * Users may redact their own events, and any user with a power level greater than or equal to the redact power level of the room may redact events there. + * + * @param txId the transaction Id + * @param roomId the room id + * @param eventId the event to delete + * @param reason json containing reason key {"reason": "Indecent material"} + */ + @PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/redact/{eventId}/{txnId}") + fun redactEvent( + @Path("txnId") txId: String, + @Path("roomId") roomId: String, + @Path("eventId") parent_id: String, + @Body reason: Map + ): Call } \ No newline at end of file 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 78d8ae8c93..c59e3f8daf 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 @@ -19,6 +19,9 @@ package im.vector.matrix.android.internal.session.room import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.internal.session.room.membership.DefaultMembershipService +import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.annotation.DefaultReactionService +import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.membership.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor import im.vector.matrix.android.internal.session.room.membership.joining.InviteTask @@ -45,6 +48,8 @@ internal class RoomFactory(private val monarchy: Monarchy, private val paginationTask: PaginationTask, private val contextOfEventTask: GetContextOfEventTask, private val setReadMarkersTask: SetReadMarkersTask, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val updateQuickReactionTask: UpdateQuickReactionTask, private val joinRoomTask: JoinRoomTask, private val leaveRoomTask: LeaveRoomTask) { @@ -53,6 +58,7 @@ internal class RoomFactory(private val monarchy: Monarchy, val timelineEventFactory = TimelineEventFactory(roomMemberExtractor, EventRelationExtractor()) val timelineService = DefaultTimelineService(roomId, monarchy, taskExecutor, timelineEventFactory, contextOfEventTask, paginationTask) val sendService = DefaultSendService(roomId, eventFactory, monarchy) + val reactionService = DefaultReactionService(roomId, eventFactory, findReactionEventForUndoTask, updateQuickReactionTask, taskExecutor) val stateService = DefaultStateService(roomId, taskExecutor, sendStateTask) val roomMembersService = DefaultMembershipService(roomId, monarchy, taskExecutor, loadRoomMembersTask, inviteTask, joinRoomTask, leaveRoomTask) val readService = DefaultReadService(roomId, monarchy, taskExecutor, setReadMarkersTask) @@ -64,6 +70,7 @@ internal class RoomFactory(private val monarchy: Monarchy, sendService, stateService, readService, + reactionService, roomMembersService ) } 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 a7fa3f4cca..8473f45e53 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 @@ -17,6 +17,10 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.internal.session.DefaultSession +import im.vector.matrix.android.internal.session.room.annotation.DefaultFindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.annotation.DefaultUpdateQuickReactionTask +import im.vector.matrix.android.internal.session.room.annotation.FindReactionEventForUndoTask +import im.vector.matrix.android.internal.session.room.annotation.UpdateQuickReactionTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask @@ -27,16 +31,14 @@ import im.vector.matrix.android.internal.session.room.membership.joining.InviteT import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.DefaultLeaveRoomTask import im.vector.matrix.android.internal.session.room.membership.leaving.LeaveRoomTask +import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTask +import im.vector.matrix.android.internal.session.room.prune.PruneEventTask import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory 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.DefaultGetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask -import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask -import im.vector.matrix.android.internal.session.room.timeline.PaginationTask -import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEventPersistor +import im.vector.matrix.android.internal.session.room.timeline.* import org.koin.dsl.module.module import retrofit2.Retrofit @@ -75,7 +77,7 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get(), get()) } scope(DefaultSession.SCOPE) { @@ -98,5 +100,17 @@ class RoomModule { DefaultSendStateTask(get()) as SendStateTask } + scope(DefaultSession.SCOPE) { + DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask + } + + scope(DefaultSession.SCOPE) { + DefaultUpdateQuickReactionTask(get()) as UpdateQuickReactionTask + } + + scope(DefaultSession.SCOPE) { + DefaultPruneEventTask(get()) as PruneEventTask + } + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt new file mode 100644 index 0000000000..dbef546166 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt @@ -0,0 +1,153 @@ +/* + * 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.annotation + +import androidx.work.* +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.room.model.annotation.ReactionService +import im.vector.matrix.android.api.util.Cancelable +import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory +import im.vector.matrix.android.internal.session.room.send.RedactEventWorker +import im.vector.matrix.android.internal.session.room.send.SendEventWorker +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith +import im.vector.matrix.android.internal.util.CancelableWork +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import java.util.concurrent.TimeUnit + +private const val REACTION_WORK = "REACTION_WORK" +private const val BACKOFF_DELAY = 10_000L + +private val WORK_CONSTRAINTS = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + +internal class DefaultReactionService(private val roomId: String, + private val eventFactory: LocalEchoEventFactory, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + private val updateQuickReactionTask: UpdateQuickReactionTask, + private val taskExecutor: TaskExecutor) + : ReactionService { + + + override fun sendReaction(reaction: String, targetEventId: String): Cancelable { + val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction) +// .also { +// //saveLocalEcho(it) +// } + val sendRelationWork = createSendRelationWork(event) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork) + .enqueue() + return CancelableWork(sendRelationWork.id) + } + + + private fun createSendRelationWork(event: Event): OneTimeWorkRequest { + //TODO use the new API to send relation (for now use regular send) + val sendContentWorkerParams = SendEventWorker.Params( + roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(sendWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ { + + val params = FindReactionEventForUndoTask.Params( + roomId, + targetEventId, + reaction, + myUserId + ) + findReactionEventForUndoTask.configureWith(params) + .enableRetry() + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: FindReactionEventForUndoTask.Result) { + data.redactEventId?.let { toRedact -> + val redactWork = createRedactEventWork(toRedact, null) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) + .enqueue() + } + } + }) + .executeBy(taskExecutor) + + } + + + override fun updateQuickReaction(reaction: String, oppositeReaction: String, targetEventId: String, myUserId: String) { + + val params = UpdateQuickReactionTask.Params( + roomId, + targetEventId, + reaction, + oppositeReaction, + myUserId + ) + + updateQuickReactionTask.configureWith(params) + .dispatchTo(object : MatrixCallback { + override fun onSuccess(data: UpdateQuickReactionTask.Result) { + data.reactionToAdd?.also { sendReaction(it, targetEventId) } + data.reactionToRedact.forEach { + val redactWork = createRedactEventWork(it, null) + WorkManager.getInstance() + .beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork) + .enqueue() + } + } + }) + .executeBy(taskExecutor) + } + + private fun buildWorkIdentifier(identifier: String): String { + return "${roomId}_$identifier" + } + +// private fun saveLocalEcho(event: Event) { +// monarchy.tryTransactionAsync { realm -> +// val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() +// ?: return@tryTransactionAsync +// val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) +// ?: return@tryTransactionAsync +// +// roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) +// } +// } + + //TODO duplicate with send service? + private fun createRedactEventWork(eventId: String, reason: String?): OneTimeWorkRequest { + + //TODO create local echo of m.room.redaction event? + + val sendContentWorkerParams = RedactEventWorker.Params( + roomId, eventId, reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return OneTimeWorkRequestBuilder() + .setConstraints(WORK_CONSTRAINTS) + .setInputData(redactWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt new file mode 100644 index 0000000000..441b289ee1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/FindReactionEventForUndoTask.kt @@ -0,0 +1,75 @@ +/* + * 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.annotation + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm + + +internal interface FindReactionEventForUndoTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val myUserId: String + ) + + data class Result( + val redactEventId: String? + ) + +} + +internal class DefaultFindReactionEventForUndoTask(private val monarchy: Monarchy) : FindReactionEventForUndoTask { + + override fun execute(params: FindReactionEventForUndoTask.Params): Try { + return Try { + var eventId: String? = null + monarchy.doWithRealm { realm -> + eventId = getReactionToRedact(realm, params.reaction, params.eventId, params.myUserId)?.eventId + } + FindReactionEventForUndoTask.Result(eventId) + } + } + + private fun getReactionToRedact(realm: Realm, reaction: String, eventId: String, userId: String): EventEntity? { + val summary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst()?.let { + //want to find the event orignated by me! + it.sourceEvents.forEach { + //find source event + EventEntity.where(realm, it).findFirst()?.let { eventEntity -> + //is it mine? + if (eventEntity.sender == userId) { + return eventEntity + } + } + } + } + } + return null + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt similarity index 65% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt index 96de9f0fb8..e262dcaf77 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/SendRelationWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/SendRelationWorker.kt @@ -1,9 +1,25 @@ -package im.vector.matrix.android.internal.session.room.send +/* + * 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.annotation import android.content.Context import androidx.work.Worker import androidx.work.WorkerParameters import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent @@ -11,6 +27,7 @@ import im.vector.matrix.android.api.session.room.model.annotation.ReactionInfo import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.util.WorkerParamsFactory import org.koin.standalone.inject @@ -28,7 +45,7 @@ class SendRelationWorker(context: Context, params: WorkerParameters) private val roomAPI by inject() override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) + val params = WorkerParamsFactory.fromData(inputData) ?: return Result.failure() val localEvent = params.event @@ -50,6 +67,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters) content = localEvent.content ) } - return result.fold({ Result.retry() }, { Result.success() }) + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> Result.failure() + } + }, { Result.success() }) } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt new file mode 100644 index 0000000000..56a8545b33 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/UpdateQuickReactionTask.kt @@ -0,0 +1,89 @@ +/* + * 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.annotation + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.task.Task +import io.realm.Realm + + +internal interface UpdateQuickReactionTask : Task { + + data class Params( + val roomId: String, + val eventId: String, + val reaction: String, + val oppositeReaction: String, + val myUserId: String + ) + + data class Result( + val reactionToAdd: String?, + val reactionToRedact: List + ) +} + +internal class DefaultUpdateQuickReactionTask(private val monarchy: Monarchy) : UpdateQuickReactionTask { + override fun execute(params: UpdateQuickReactionTask.Params): Try { + return Try { + var res: Pair?>? = null + monarchy.doWithRealm { realm -> + res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId, params.myUserId) + } + UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) + } + } + + + private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String, myUserId: String): Pair?> { + //the emoji reaction has been selected, we need to check if we have reacted it or not + val existingSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst() + ?: return Pair(reaction, null) + + //Ok there is already reactions on this event, have we reacted to it + val aggregationForReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reaction) + .findFirst() + val aggregationForOppositeReaction = existingSummary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, oppositeReaction) + .findFirst() + + if (aggregationForReaction == null || !aggregationForReaction.addedByMe) { + //i haven't yet reacted to it, so need to add it, but do I need to redact the opposite? + val toRedact = aggregationForOppositeReaction?.sourceEvents?.mapNotNull { + //find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == myUserId) entity.eventId else null + } + return Pair(reaction, toRedact) + } else { + //I already added it, so i need to undo it (like a toggle) + // find all m.redaction coming from me to readact them + val toRedact = aggregationForReaction.sourceEvents.mapNotNull { + //find source event + val entity = EventEntity.where(realm, it).findFirst() + if (entity?.sender == myUserId) entity.eventId else null + } + return Pair(null, toRedact) + } + + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt index ad17032ae3..cb7a38184b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/EventsPruner.kt @@ -16,38 +16,37 @@ package im.vector.matrix.android.internal.session.room.prune -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.util.WorkerParamsFactory +import im.vector.matrix.android.internal.task.TaskExecutor +import im.vector.matrix.android.internal.task.configureWith -private const val PRUNE_EVENT_WORKER = "PRUNE_EVENT_WORKER" -internal class EventsPruner(monarchy: Monarchy) : +internal class EventsPruner(monarchy: Monarchy, + private val credentials: Credentials, + private val pruneEventTask: PruneEventTask, + private val taskExecutor: TaskExecutor) : RealmLiveEntityObserver(monarchy) { override val query = Monarchy.Query { EventEntity.where(it, type = EventType.REDACTION) } override fun processChanges(inserted: List, updated: List, deleted: List) { val redactionEvents = inserted - .mapNotNull { it.asDomain().redacts } + .mapNotNull { it.asDomain() } - val pruneEventWorkerParams = PruneEventWorker.Params(redactionEvents) - val workData = WorkerParamsFactory.toData(pruneEventWorkerParams) + val params = PruneEventTask.Params( + redactionEvents, + credentials.userId + ) - val sendWork = OneTimeWorkRequestBuilder() - .setInputData(workData) - .build() + pruneEventTask.configureWith(params) + .executeBy(taskExecutor) - WorkManager.getInstance() - .beginUniqueWork(PRUNE_EVENT_WORKER, ExistingWorkPolicy.APPEND, sendWork) - .enqueue() } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt new file mode 100644 index 0000000000..82949b41d1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventTask.kt @@ -0,0 +1,142 @@ +/* + * 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.prune + +import arrow.core.Try +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.events.model.Event +import im.vector.matrix.android.api.session.events.model.EventType +import im.vector.matrix.android.api.session.events.model.UnsignedData +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.annotation.ReactionContent +import im.vector.matrix.android.internal.database.mapper.ContentMapper +import im.vector.matrix.android.internal.database.mapper.EventMapper +import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity +import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields +import im.vector.matrix.android.internal.database.query.where +import im.vector.matrix.android.internal.di.MoshiProvider +import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionSync +import io.realm.Realm +import timber.log.Timber + + +internal interface PruneEventTask : Task { + + data class Params( + val redactionEvents: List, + val userId: String + ) + +} + +internal class DefaultPruneEventTask(private val monarchy: Monarchy) : PruneEventTask { + + override fun execute(params: PruneEventTask.Params): Try { + return monarchy.tryTransactionSync { realm -> + params.redactionEvents.forEach { event -> + pruneEvent(realm, event, params.userId) + } + } + } + + private fun pruneEvent(realm: Realm, redactionEvent: Event, userId: String) { + if (redactionEvent.redacts.isNullOrBlank()) { + return + } + + val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() + ?: return + + val allowedKeys = computeAllowedKeys(eventToPrune.type) + if (allowedKeys.isNotEmpty()) { + val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } + eventToPrune.content = ContentMapper.map(prunedContent) + } else { + when (eventToPrune.type) { + EventType.MESSAGE -> { + Timber.d("REDACTION for message ${eventToPrune.eventId}") + val unsignedData = EventMapper.map(eventToPrune).unsignedData + ?: UnsignedData(null, null) + val modified = unsignedData.copy(redactedEvent = redactionEvent) + eventToPrune.content = ContentMapper.map(emptyMap()) + eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + + } + EventType.REACTION -> { + Timber.d("REDACTION of reaction ${eventToPrune.eventId}") + //delete a reaction, need to update the annotation summary if any + val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel() + ?: return + val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return + + val reactionkey = reactionContent.relatesTo.key + Timber.d("REMOVE reaction for key $reactionkey") + val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst() + if (summary != null) { + summary.reactionsSummary.where() + .equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey) + .findFirst()?.let { summary -> + Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})") + Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}") + if (summary.sourceEvents.contains(eventToPrune.eventId)) { + Timber.d("REMOVE reaction for key $reactionkey") + summary.sourceEvents.remove(eventToPrune.eventId) + Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}") + summary.count = summary.count - 1 + if (eventToPrune.sender == userId) { + //Was it a redact on my reaction? + summary.addedByMe = false + } + if (summary.count == 0) { + //delete! + summary.deleteFromRealm() + } + } else { + Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known") + } + } + } else { + Timber.e("## Cannot find summary for key $reactionkey") + } + } + } + } + } + + private fun computeAllowedKeys(type: String): List { + // Add filtered content, allowed keys in content depends on the event type + return when (type) { + EventType.STATE_ROOM_MEMBER -> listOf("membership") + EventType.STATE_ROOM_CREATE -> listOf("creator") + EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") + EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", + "users_default", + "events", + "events_default", + "state_default", + "ban", + "kick", + "redact", + "invite") + EventType.STATE_ROOM_ALIASES -> listOf("aliases") + EventType.STATE_CANONICAL_ALIAS -> listOf("alias") + EventType.FEEDBACK -> listOf("type", "target_event_id") + else -> emptyList() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt deleted file mode 100644 index 245aa551f7..0000000000 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/prune/PruneEventWorker.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.prune - -import android.content.Context -import androidx.work.Worker -import androidx.work.WorkerParameters -import com.squareup.moshi.JsonClass -import com.zhuinden.monarchy.Monarchy -import im.vector.matrix.android.api.session.events.model.EventType -import im.vector.matrix.android.internal.database.mapper.ContentMapper -import im.vector.matrix.android.internal.database.model.EventEntity -import im.vector.matrix.android.internal.database.query.where -import im.vector.matrix.android.internal.di.MatrixKoinComponent -import im.vector.matrix.android.internal.util.WorkerParamsFactory -import im.vector.matrix.android.internal.util.tryTransactionSync -import io.realm.Realm -import org.koin.standalone.inject - -internal class PruneEventWorker(context: Context, - workerParameters: WorkerParameters -) : Worker(context, workerParameters), MatrixKoinComponent { - - @JsonClass(generateAdapter = true) - internal class Params( - val eventIdsToRedact: List - ) - - private val monarchy by inject() - - override fun doWork(): Result { - val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() - - val result = monarchy.tryTransactionSync { realm -> - params.eventIdsToRedact.forEach { eventId -> - pruneEvent(realm, eventId) - } - } - return result.fold({ Result.retry() }, { Result.success() }) - } - - private fun pruneEvent(realm: Realm, eventIdToRedact: String) { - if (eventIdToRedact.isEmpty()) { - return - } - - val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst() - ?: return - - val allowedKeys = computeAllowedKeys(eventToPrune.type) - if (allowedKeys.isNotEmpty()) { - val prunedContent = ContentMapper.map(eventToPrune.content)?.filterKeys { key -> allowedKeys.contains(key) } - eventToPrune.content = ContentMapper.map(prunedContent) - } - } - - private fun computeAllowedKeys(type: String): List { - // Add filtered content, allowed keys in content depends on the event type - return when (type) { - EventType.STATE_ROOM_MEMBER -> listOf("membership") - EventType.STATE_ROOM_CREATE -> listOf("creator") - EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") - EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", - "users_default", - "events", - "events_default", - "state_default", - "ban", - "kick", - "redact", - "invite") - EventType.STATE_ROOM_ALIASES -> listOf("aliases") - EventType.STATE_CANONICAL_ALIAS -> listOf("alias") - EventType.FEEDBACK -> listOf("type", "target_event_id") - else -> emptyList() - } - } - -} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 916077dff7..28ae63b88c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -16,13 +16,7 @@ package im.vector.matrix.android.internal.session.room.send -import androidx.work.BackoffPolicy -import androidx.work.Constraints -import androidx.work.ExistingWorkPolicy -import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest -import androidx.work.OneTimeWorkRequestBuilder -import androidx.work.WorkManager +import androidx.work.* import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event @@ -74,16 +68,14 @@ internal class DefaultSendService(private val roomId: String, return cancelableBag } - - override fun sendReaction(reaction: String, targetEventId: String) : Cancelable { - val event = eventFactory.createReactionEvent(roomId,targetEventId,reaction).also { - saveLocalEcho(it) - } - val sendRelationWork = createSendRelationWork(event) + override fun redactEvent(event: Event, reason: String?): Cancelable { + //TODO manage local echo ? + //TODO manage media/attachements? + val redactWork = createRedactEventWork(event, reason) WorkManager.getInstance() - .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendRelationWork) + .beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork) .enqueue() - return CancelableWork(sendRelationWork.id) + return CancelableWork(redactWork.id) } override fun sendMedia(attachment: ContentAttachmentData): Cancelable { @@ -105,9 +97,9 @@ internal class DefaultSendService(private val roomId: String, private fun saveLocalEcho(event: Event) { monarchy.tryTransactionAsync { realm -> val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() - ?: return@tryTransactionAsync + ?: return@tryTransactionAsync val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId) - ?: return@tryTransactionAsync + ?: return@tryTransactionAsync roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0) } @@ -128,15 +120,17 @@ internal class DefaultSendService(private val roomId: String, .build() } - private fun createSendRelationWork(event: Event): OneTimeWorkRequest { - //TODO use the new API to send relation (for now use regular send) - val sendContentWorkerParams = SendEventWorker.Params( - roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest { - return OneTimeWorkRequestBuilder() + //TODO create local echo of m.room.redaction event? + + val sendContentWorkerParams = RedactEventWorker.Params( + roomId, event.eventId!!, reason) + val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return OneTimeWorkRequestBuilder() .setConstraints(WORK_CONSTRAINTS) - .setInputData(sendWorkData) + .setInputData(redactWorkData) .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) .build() } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt new file mode 100644 index 0000000000..e0bc740ee7 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/RedactEventWorker.kt @@ -0,0 +1,69 @@ +/* + * 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.send + +import android.content.Context +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.failure.Failure +import im.vector.matrix.android.internal.di.MatrixKoinComponent +import im.vector.matrix.android.internal.network.executeRequest +import im.vector.matrix.android.internal.session.room.RoomAPI +import im.vector.matrix.android.internal.util.WorkerParamsFactory +import org.koin.standalone.inject +import java.util.* + +internal class RedactEventWorker(context: Context, params: WorkerParameters) + : Worker(context, params), MatrixKoinComponent { + + @JsonClass(generateAdapter = true) + internal data class Params( + val roomId: String, + val eventId: String, + val reason: String? + ) + + private val roomAPI by inject() + + override fun doWork(): Result { + val params = WorkerParamsFactory.fromData(inputData) + ?: return Result.failure() + + if (params.eventId == null) { + return Result.failure() + } + val txID = UUID.randomUUID().toString() + + val result = executeRequest { + apiCall = roomAPI.redactEvent( + txID, + params.roomId, + params.eventId, + if (params.reason == null) emptyMap() else mapOf("reason" to params.reason) + ) + } + return result.fold({ + when (it) { + is Failure.NetworkConnection -> Result.retry() + else -> Result.failure() + } + }, { + Result.success() + }) + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 98a1ab305c..6743d1cb0b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -92,7 +92,7 @@ internal class DefaultTimeline( private lateinit var eventRelations: RealmResults - private val eventsChangeListener = OrderedRealmCollectionChangeListener> { _, changeSet -> + private val eventsChangeListener = OrderedRealmCollectionChangeListener> { results, changeSet -> if (changeSet.state == OrderedCollectionChangeSet.State.INITIAL) { handleInitialLoad() } else { @@ -122,8 +122,22 @@ internal class DefaultTimeline( buildTimelineEvents(startDisplayIndex, direction, range.length.toLong()) postSnapshot() } - } + + var hasChanged = false + changeSet.changes.forEach {index -> + val eventEntity = results[index] + eventEntity?.eventId?.let { eventId -> + builtEventsIdMap[eventId]?.let { builtIndex -> + //Update the relation of existing event + builtEvents[builtIndex]?.let { te -> + builtEvents[builtIndex] = timelineEventFactory.create(eventEntity) + hasChanged = true + } + } + } + } + if (hasChanged) postSnapshot() } } @@ -143,18 +157,6 @@ internal class DefaultTimeline( } } } - changeSet.deletions?.forEach { - val eventRelations = collection[it] - if (eventRelations != null) { - builtEventsIdMap[eventRelations.eventId]?.let { builtIndex -> - //Update the relation of existing event - builtEvents[builtIndex]?.let { te -> - builtEvents[builtIndex] = te.copy(annotations = null) - hasChange = true - } - } - } - } if (hasChange) postSnapshot() } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 4461ce957c..10552231db 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -28,7 +28,11 @@ sealed class RoomDetailActions { data class EventDisplayed(val event: TimelineEvent) : RoomDetailActions() data class LoadMore(val direction: Timeline.Direction) : RoomDetailActions() data class SendReaction(val reaction: String, val targetEventId: String) : RoomDetailActions() + data class RedactAction(val targetEventId: String, val reason: String? = "") : RoomDetailActions() + data class UndoReaction(val targetEventId: String, val key: String, val reason: String? = "") : RoomDetailActions() + data class UpdateQuickReactAction(val targetEventId: String,val selectedReaction: String,val opposite: String) : RoomDetailActions() object AcceptInvite : RoomDetailActions() object RejectInvite : RoomDetailActions() + } \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index 91e77a6115..72141e8ad5 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -54,11 +54,7 @@ import com.otaliastudios.autocomplete.AutocompleteCallback import com.otaliastudios.autocomplete.CharPolicy import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership -import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent -import im.vector.matrix.android.api.session.room.model.message.MessageContent -import im.vector.matrix.android.api.session.room.model.message.MessageFileContent -import im.vector.matrix.android.api.session.room.model.message.MessageImageContent -import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.user.model.User import im.vector.riotredesign.R @@ -69,15 +65,7 @@ import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.VectorBaseFragment -import im.vector.riotredesign.core.utils.LiveEvent -import im.vector.riotredesign.core.utils.PERMISSIONS_FOR_TAKING_PHOTO -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA -import im.vector.riotredesign.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA -import im.vector.riotredesign.core.utils.checkPermissions -import im.vector.riotredesign.core.utils.copyToClipboard -import im.vector.riotredesign.core.utils.openCamera -import im.vector.riotredesign.core.utils.shareMedia +import im.vector.riotredesign.core.utils.* import im.vector.riotredesign.features.autocomplete.command.AutocompleteCommandPresenter import im.vector.riotredesign.features.autocomplete.command.CommandAutocompletePolicy import im.vector.riotredesign.features.autocomplete.user.AutocompleteUserPresenter @@ -199,11 +187,11 @@ class RoomDetailFragment : if (resultCode == RESULT_OK && data != null) { when (requestCode) { REQUEST_FILES_REQUEST_CODE, TAKE_IMAGE_REQUEST_CODE -> handleMediaIntent(data) - REACTION_SELECT_REQUEST_CODE -> { + REACTION_SELECT_REQUEST_CODE -> { val eventId = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_EVENT_ID) - ?: return + ?: return val reaction = data.getStringExtra(EmojiReactionPickerActivity.EXTRA_REACTION_RESULT) - ?: return + ?: return //TODO check if already reacted with that? roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, eventId)) } @@ -367,24 +355,24 @@ class RoomDetailFragment : private fun onSendChoiceClicked(dialogListItem: DialogListItem) { Timber.v("On send choice clicked: $dialogListItem") when (dialogListItem) { - is DialogListItem.SendFile -> { + is DialogListItem.SendFile -> { // launchFileIntent } - is DialogListItem.SendVoice -> { + is DialogListItem.SendVoice -> { //launchAudioRecorderIntent() } - is DialogListItem.SendSticker -> { + is DialogListItem.SendSticker -> { //startStickerPickerActivity() } is DialogListItem.TakePhotoVideo -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_CAMERA)) { // launchCamera() } - is DialogListItem.TakePhoto -> + is DialogListItem.TakePhoto -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA)) { openCamera(requireActivity(), CAMERA_VALUE_TITLE, TAKE_IMAGE_REQUEST_CODE) } - is DialogListItem.TakeVideo -> + is DialogListItem.TakeVideo -> if (checkPermissions(PERMISSIONS_FOR_TAKING_PHOTO, requireActivity(), PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA)) { // launchNativeVideoRecorder() } @@ -431,20 +419,20 @@ class RoomDetailFragment : private fun renderSendMessageResult(sendMessageResult: SendMessageResult) { when (sendMessageResult) { is SendMessageResult.MessageSent, - is SendMessageResult.SlashCommandHandled -> { + is SendMessageResult.SlashCommandHandled -> { // Clear composer composerEditText.text = null } - is SendMessageResult.SlashCommandError -> { + is SendMessageResult.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) } - is SendMessageResult.SlashCommandUnknown -> { + is SendMessageResult.SlashCommandUnknown -> { displayCommandError(getString(R.string.unrecognized_command, sendMessageResult.command)) } - is SendMessageResult.SlashCommandResultOk -> { + is SendMessageResult.SlashCommandResultOk -> { // Ignore } - is SendMessageResult.SlashCommandResultError -> { + is SendMessageResult.SlashCommandResultError -> { displayCommandError(sendMessageResult.throwable.localizedMessage) } is SendMessageResult.SlashCommandNotImplemented -> { @@ -521,7 +509,8 @@ class RoomDetailFragment : //we should test the current real state of reaction on this event roomDetailViewModel.process(RoomDetailActions.SendReaction(reaction, informationData.eventId)) } else { - //TODO it's an undo :/ + //I need to redact a reaction + roomDetailViewModel.process(RoomDetailActions.UndoReaction(informationData.eventId, reaction)) } } @@ -535,18 +524,22 @@ class RoomDetailFragment : it?.getContentIfNotHandled()?.let { actionData -> when (actionData.actionId) { - MessageMenuViewModel.ACTION_ADD_REACTION -> { + MessageMenuViewModel.ACTION_ADD_REACTION -> { val eventId = actionData.data?.toString() ?: return startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), eventId), REACTION_SELECT_REQUEST_CODE) } - MessageMenuViewModel.ACTION_COPY -> { + MessageMenuViewModel.ACTION_COPY -> { //I need info about the current selected message :/ copyToClipboard(requireContext(), actionData.data?.toString() ?: "", false) val snack = Snackbar.make(view!!, requireContext().getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) snack.view.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.notification_accent_color)) snack.show() } - MessageMenuViewModel.ACTION_SHARE -> { + MessageMenuViewModel.ACTION_DELETE -> { + val eventId = actionData.data?.toString() ?: return + roomDetailViewModel.process(RoomDetailActions.RedactAction(eventId, context?.getString(R.string.event_redacted_by_user_reason))) + } + MessageMenuViewModel.ACTION_SHARE -> { //TODO current data communication is too limited //Need to now the media type actionData.data?.toString()?.let { @@ -589,12 +582,13 @@ class RoomDetailFragment : .setPositiveButton(R.string.ok) { dialog, id -> dialog.cancel() } .show() } - MessageMenuViewModel.ACTION_QUICK_REACT -> { - (actionData.data as? Pair)?.let { pairData -> - roomDetailViewModel.process(RoomDetailActions.SendReaction(pairData.second, pairData.first)) + MessageMenuViewModel.ACTION_QUICK_REACT -> { + //eventId,ClickedOn,Opposite + (actionData.data as? Triple)?.let { (eventId, clickedOn, opposite) -> + roomDetailViewModel.process(RoomDetailActions.UpdateQuickReactAction(eventId, clickedOn, opposite)) } } - else -> { + else -> { Toast.makeText(context, "Action ${actionData.actionId} not implemented", Toast.LENGTH_LONG).show() } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 6b6ac2ccca..1c185adda2 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -72,17 +72,21 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> handleIsDisplayed() - is RoomDetailActions.SendMedia -> handleSendMedia(action) + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.SendMedia -> handleSendMedia(action) is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) - is RoomDetailActions.LoadMore -> handleLoadMore(action) - is RoomDetailActions.SendReaction -> handleSendReaction(action) - is RoomDetailActions.AcceptInvite -> handleAcceptInvite() - is RoomDetailActions.RejectInvite -> handleRejectInvite() + is RoomDetailActions.LoadMore -> handleLoadMore(action) + is RoomDetailActions.SendReaction -> handleSendReaction(action) + is RoomDetailActions.AcceptInvite -> handleAcceptInvite() + is RoomDetailActions.RejectInvite -> handleRejectInvite() + is RoomDetailActions.RedactAction -> handleRedactEvent(action) + is RoomDetailActions.UndoReaction -> handleUndoReact(action) + is RoomDetailActions.UpdateQuickReactAction -> handleUpdateQuickReaction(action) } } + private val _sendMessageResultLiveData = MutableLiveData>() val sendMessageResultLiveData: LiveData> get() = _sendMessageResultLiveData @@ -94,63 +98,63 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, val slashCommandResult = CommandParser.parseSplashCommand(action.text) when (slashCommandResult) { - is ParsedCommand.ErrorNotACommand -> { + is ParsedCommand.ErrorNotACommand -> { // Send the text message to the room room.sendTextMessage(action.text) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.MessageSent)) } - is ParsedCommand.ErrorSyntax -> { + is ParsedCommand.ErrorSyntax -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandError(slashCommandResult.command))) } - is ParsedCommand.ErrorEmptySlashCommand -> { + is ParsedCommand.ErrorEmptySlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown("/"))) } is ParsedCommand.ErrorUnknownSlashCommand -> { _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandUnknown(slashCommandResult.slashCommand))) } - is ParsedCommand.Invite -> { + is ParsedCommand.Invite -> { handleInviteSlashCommand(slashCommandResult) } - is ParsedCommand.SetUserPowerLevel -> { + is ParsedCommand.SetUserPowerLevel -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.ClearScalarToken -> { + is ParsedCommand.ClearScalarToken -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.SetMarkdown -> { + is ParsedCommand.SetMarkdown -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.UnbanUser -> { + is ParsedCommand.UnbanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.BanUser -> { + is ParsedCommand.BanUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.KickUser -> { + is ParsedCommand.KickUser -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.JoinRoom -> { + is ParsedCommand.JoinRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.PartRoom -> { + is ParsedCommand.PartRoom -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } - is ParsedCommand.SendEmote -> { + is ParsedCommand.SendEmote -> { room.sendTextMessage(slashCommandResult.message, msgType = MessageType.MSGTYPE_EMOTE) _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandHandled)) } - is ParsedCommand.ChangeTopic -> { + is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) } - is ParsedCommand.ChangeDisplayName -> { + is ParsedCommand.ChangeDisplayName -> { // TODO _sendMessageResultLiveData.postValue(LiveEvent(SendMessageResult.SlashCommandNotImplemented)) } @@ -190,6 +194,21 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.sendReaction(action.reaction, action.targetEventId) } + private fun handleRedactEvent(action: RoomDetailActions.RedactAction) { + val event = room.getTimeLineEvent(action.targetEventId) ?: return + room.redactEvent(event.root, action.reason) + } + + private fun handleUndoReact(action: RoomDetailActions.UndoReaction) { + room.undoReaction(action.key, action.targetEventId, session.sessionParams.credentials.userId) + } + + + private fun handleUpdateQuickReaction(action: RoomDetailActions.UpdateQuickReactAction) { + room.updateQuickReaction(action.selectedReaction, action.opposite, action.targetEventId, session.sessionParams.credentials.userId) + } + + private fun handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { ContentAttachmentData( @@ -227,6 +246,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.join(object : MatrixCallback {}) } + private fun observeEventDisplayedActions() { // We are buffering scroll events for one second // and keep the most recent one to set the read receipt on. diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt index c1576eef11..1d50d893f0 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageActionsBottomSheet.kt @@ -95,13 +95,8 @@ class MessageActionsBottomSheet : BaseMvRxBottomSheetDialog() { .commit() } quickReactionFragment.interactionListener = object : QuickReactionFragment.InteractionListener { - override fun didQuickReactWith(clikedOn: String, reactions: List, eventId: String) { - if (reactions.contains(clikedOn)) { - //it's an add - actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Pair(eventId,clikedOn)) - } else { - //it's a remove - } + override fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List, eventId: String) { + actionHandlerModel.fireAction(MessageMenuViewModel.ACTION_QUICK_REACT, Triple(eventId, clikedOn, opposite)) dismiss() } } diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 9f52306272..c39da48447 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -73,6 +73,10 @@ class MessageMenuViewModel(initialState: MessageMenuState) : VectorViewModel, eventId: String) + fun didQuickReactWith(clikedOn: String, opposite: String, reactions: List, eventId: String) } companion object { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt index 8284c16c6b..36a07bee59 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/action/QuickReactionViewModel.kt @@ -110,6 +110,16 @@ class QuickReactionViewModel(initialState: QuickReactionState) : VectorViewModel val likePositive = "🙂" val likeNegative = "😔" + fun getOpposite(reaction: String): String? { + return when (reaction) { + agreePositive -> agreeNegative + agreeNegative -> agreePositive + likePositive -> likeNegative + likeNegative -> likePositive + else -> null + } + } + override fun initialState(viewModelContext: ViewModelContext): QuickReactionState? { // Args are accessible from the context. // val foo = vieWModelContext.args.foo diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 924c34813d..b4374c0c6e 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -66,7 +66,6 @@ class MessageItemFactory(private val colorProvider: ColorProvider, || nextEvent?.root?.type != EventType.MESSAGE || isNextMessageReceivedMoreThanOneHourAgo - val messageContent: MessageContent = event.root.content.toModel() ?: return null val time = timelineDateFormatter.formatMessageHour(date) val avatarUrl = event.senderAvatar val memberName = event.senderName ?: event.root.sender ?: "" @@ -84,8 +83,12 @@ class MessageItemFactory(private val colorProvider: ColorProvider, orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) } ) - //Test for reactions UX - //informationData.orderedReactionList = listOf( Triple("👍",1,false), Triple("👎",2,false)) + if (event.root.unsignedData?.redactedEvent != null) { + //message is redacted + return buildRedactedItem(informationData, callback) + } + + val messageContent: MessageContent = event.root.content.toModel() ?: return null // val all = event.root.toContent() // val ev = all.toModel() @@ -347,6 +350,19 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } + private fun buildRedactedItem(informationData: MessageInformationData, callback: TimelineEventController.Callback?): RedactedMessageItem? { + return RedactedMessageItem_() + .informationData(informationData) + .avatarClickListener( + DebouncedClickListener(View.OnClickListener { view -> + callback?.onAvatarClicked(informationData) + })) + .memberClickListener( + DebouncedClickListener(View.OnClickListener { view -> + callback?.onMemberNameClicked(informationData) + })) + } + private fun linkifyBody(body: CharSequence, callback: TimelineEventController.Callback?): CharSequence { val spannable = SpannableStringBuilder(body) MatrixLinkify.addLinks(spannable, object : MatrixPermalinkSpan.Callback { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt index 6db0e0eb39..4e219fadc6 100644 --- a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/AbsMessageItem.kt @@ -55,11 +55,11 @@ abstract class AbsMessageItem : BaseEventItem() { var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { override fun onReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString,true) + reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, true) } override fun onUnReacted(reactionButton: ReactionButton) { - reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString,false) + reactionPillCallback?.onClickOnReactionPill(informationData, reactionButton.reactionString, false) } } @@ -80,17 +80,23 @@ abstract class AbsMessageItem : BaseEventItem() { holder.timeView.text = informationData.time holder.memberNameView.text = informationData.memberName AvatarRenderer.render(informationData.avatarUrl, informationData.senderId, informationData.memberName?.toString(), holder.avatarImageView) + holder.view.setOnClickListener(cellClickListener) + holder.view.setOnLongClickListener(longClickListener) + holder.avatarImageView.setOnLongClickListener(longClickListener) + holder.memberNameView.setOnLongClickListener(longClickListener) } else { holder.avatarImageView.setOnClickListener(null) holder.memberNameView.setOnClickListener(null) holder.avatarImageView.visibility = View.GONE holder.memberNameView.visibility = View.GONE holder.timeView.visibility = View.GONE + holder.view.setOnClickListener(null) + holder.view.setOnLongClickListener(null) + holder.avatarImageView.setOnLongClickListener(null) + holder.memberNameView.setOnLongClickListener(null) } - holder.view.setOnClickListener(cellClickListener) - holder.view.setOnLongClickListener(longClickListener) - if (informationData.orderedReactionList.isNullOrEmpty()) { + if (!shouldShowReactionAtBottom() || informationData.orderedReactionList.isNullOrEmpty()) { holder.reactionWrapper?.isVisible = false } else { //inflate if needed @@ -119,12 +125,12 @@ abstract class AbsMessageItem : BaseEventItem() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && !holder.view.isInLayout) { holder.reactionFlowHelper?.requestLayout() } - + holder.reactionWrapper?.setOnLongClickListener(longClickListener) } } - override fun unbind(holder: H) { - super.unbind(holder) + open fun shouldShowReactionAtBottom() : Boolean { + return true } protected fun View.renderSendState() { diff --git a/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RedactedMessageItem.kt b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RedactedMessageItem.kt new file mode 100644 index 0000000000..7331a6f34b --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RedactedMessageItem.kt @@ -0,0 +1,24 @@ +package im.vector.riotredesign.features.home.room.detail.timeline.item + +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.riotredesign.R + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class RedactedMessageItem : AbsMessageItem() { + + @EpoxyAttribute + override lateinit var informationData: MessageInformationData + + override fun getStubType(): Int = STUB_ID + + override fun shouldShowReactionAtBottom() = false + + class Holder : AbsMessageItem.Holder() { + override fun getStubId(): Int = STUB_ID + } + + companion object { + private val STUB_ID = R.id.messageContentRedactedStub + } +} \ No newline at end of file diff --git a/vector/src/main/res/drawable/redacted_background.xml b/vector/src/main/res/drawable/redacted_background.xml new file mode 100644 index 0000000000..8538e15988 --- /dev/null +++ b/vector/src/main/res/drawable/redacted_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml index ba39f58c0f..fef7b0eeef 100644 --- a/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml +++ b/vector/src/main/res/layout/adapter_item_action_quick_reaction.xml @@ -15,6 +15,7 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" + android:textColor="@color/black" android:textSize="30sp" app:autoSizeTextType="uniform" app:layout_constraintBottom_toTopOf="@id/quick_react_agree_text" @@ -34,10 +35,10 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" + android:textColor="@color/black" android:textSize="30sp" app:autoSizeTextType="uniform" app:layout_constraintBottom_toBottomOf="@id/quick_react_1_text" - app:layout_constraintEnd_toStartOf="@id/center_guideline" app:layout_constraintStart_toEndOf="@id/quick_react_1_text" app:layout_constraintTop_toTopOf="@id/quick_react_1_text" @@ -73,6 +74,7 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" + android:textColor="@color/black" android:textSize="30sp" app:autoSizeTextType="uniform" app:layout_constraintBottom_toBottomOf="@+id/quick_react_1_text" @@ -91,6 +93,7 @@ android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" + android:textColor="@color/black" android:textSize="30sp" app:autoSizeTextType="uniform" app:layout_constraintBottom_toBottomOf="@id/quick_react_3_text" diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index dc2e2ed67e..2ec5cf041a 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -81,21 +81,29 @@ android:layout="@layout/item_timeline_event_file_stub" tools:ignore="MissingConstraints" /> + diff --git a/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml new file mode 100644 index 0000000000..2f930577f0 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/vector/src/main/res/layout/reaction_button.xml b/vector/src/main/res/layout/reaction_button.xml index 8d66d98776..70b841bc9d 100644 --- a/vector/src/main/res/layout/reaction_button.xml +++ b/vector/src/main/res/layout/reaction_button.xml @@ -2,9 +2,9 @@ + android:layout_height="26dp" + android:clipChildren="false"> + app:layout_constraintTop_toTopOf="@+id/reactionText" /> + app:layout_constraintTop_toTopOf="@+id/reactionText" /> + tools:text="👍" /> Like Add Reaction + Event deleted by user + Event moderated by room admin + \ No newline at end of file