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 ba1095f5..7390bc4e 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 004495b5..4a9547e3 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 2061a296..ae890eaf 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 00000000..154401f9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/annotation/ReactionService.kt @@ -0,0 +1,38 @@ +/* + * 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 + */ + fun undoReaction(reaction: String, targetEventId: String, myUserId: String)//: Cancelable + +} \ 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 7cd2dbf0..6852931c 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/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index c767e5c6..80d89659 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 0df08914..85e7245d 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 23e74c74..39d9c4d3 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 78d8ae8c..1f5cd18d 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,8 @@ 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.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 +47,7 @@ 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 joinRoomTask: JoinRoomTask, private val leaveRoomTask: LeaveRoomTask) { @@ -53,6 +56,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, monarchy, findReactionEventForUndoTask, 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 +68,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 a7fa3f4c..c7df624c 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,8 @@ 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.FindReactionEventForUndoTask 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 @@ -98,5 +100,10 @@ class RoomModule { DefaultSendStateTask(get()) as SendStateTask } + scope(DefaultSession.SCOPE) { + DefaultFindReactionEventForUndoTask(get()) as FindReactionEventForUndoTask + } + + } } 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 00000000..1afbe7df --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/annotation/DefaultReactionService.kt @@ -0,0 +1,133 @@ +/* + * 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 com.zhuinden.monarchy.Monarchy +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.database.helper.addSendingEvent +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom +import im.vector.matrix.android.internal.database.query.where +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 im.vector.matrix.android.internal.util.tryTransactionAsync +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 monarchy: Monarchy, + private val findReactionEventForUndoTask: FindReactionEventForUndoTask, + 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) + + } + + 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 00000000..441b289e --- /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 71% 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 96de9f0f..7ce871a0 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,4 +1,19 @@ -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 @@ -11,6 +26,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 +44,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 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 ad17032a..d6792f30 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 @@ -36,7 +36,7 @@ internal class EventsPruner(monarchy: Monarchy) : 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) 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 index 245aa551..a65988a5 100644 --- 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 @@ -21,15 +21,24 @@ 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.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.MatrixKoinComponent +import im.vector.matrix.android.internal.di.MoshiProvider 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 +import timber.log.Timber internal class PruneEventWorker(context: Context, workerParameters: WorkerParameters @@ -37,57 +46,105 @@ internal class PruneEventWorker(context: Context, @JsonClass(generateAdapter = true) internal class Params( - val eventIdsToRedact: List + val redactionEvents: List ) private val monarchy by inject() override fun doWork(): Result { val params = WorkerParamsFactory.fromData(inputData) - ?: return Result.failure() + ?: return Result.failure() val result = monarchy.tryTransactionSync { realm -> - params.eventIdsToRedact.forEach { eventId -> - pruneEvent(realm, eventId) + params.redactionEvents.forEach { event -> + pruneEvent(realm, event) } } - return result.fold({ Result.retry() }, { Result.success() }) + return result.fold({ + Result.retry() + }, { + Result.success() + }) } - private fun pruneEvent(realm: Realm, eventIdToRedact: String) { - if (eventIdToRedact.isEmpty()) { + private fun pruneEvent(realm: Realm, redactionEvent: Event) { + if (redactionEvent.redacts.isNullOrBlank()) { return } - val eventToPrune = EventEntity.where(realm, eventId = eventIdToRedact).findFirst() - ?: 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 (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_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() + "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() } } 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 916077df..28ae63b8 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 00000000..e0bc740e --- /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 98a1ab30..378880f4 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() } } 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 4461ce95..b6ebf330 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,10 @@ 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() 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 91e77a61..c9c013a8 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 @@ -521,7 +521,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)) } } @@ -546,7 +547,11 @@ class RoomDetailFragment : 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 { 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 6b6ac2cc..15a5e2b5 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 @@ -80,6 +80,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, is RoomDetailActions.SendReaction -> handleSendReaction(action) is RoomDetailActions.AcceptInvite -> handleAcceptInvite() is RoomDetailActions.RejectInvite -> handleRejectInvite() + is RoomDetailActions.RedactAction -> handleRedactEvent(action) } } @@ -190,6 +191,16 @@ 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 handleSendMedia(action: RoomDetailActions.SendMedia) { val attachments = action.mediaFiles.map { ContentAttachmentData( 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 9f523062..67e45bc7 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() @@ -347,6 +350,11 @@ class MessageItemFactory(private val colorProvider: ColorProvider, } } + private fun buildRedactedItem(informationData: MessageInformationData): RedactedMessageItem? { + return RedactedMessageItem_() + .informationData(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 6db0e0eb..ee0f6e41 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) } } @@ -123,10 +123,6 @@ abstract class AbsMessageItem : BaseEventItem() { } } - override fun unbind(holder: H) { - super.unbind(holder) - } - protected fun View.renderSendState() { isClickable = informationData.sendState.isSent() alpha = if (informationData.sendState.isSent()) 1f else 0.5f 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 00000000..e7132844 --- /dev/null +++ b/vector/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/item/RedactedMessageItem.kt @@ -0,0 +1,22 @@ +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 + + 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 00000000..8538e159 --- /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/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index dc2e2ed6..806e18e3 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -81,6 +81,12 @@ 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 00000000..948e6ea6 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_redacted_stub.xml @@ -0,0 +1,5 @@ + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 45131c75..8a9e11d9 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -13,4 +13,7 @@ Like Add Reaction + Event deleted by user + Event moderated by room admin + \ No newline at end of file