diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt index 108ce056eb..4d06b737e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/model/event/EncryptedEventContent.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent /** * Class representing an encrypted event content @@ -52,5 +53,8 @@ data class EncryptedEventContent( * The session id */ @Json(name = "session_id") - val sessionId: String? = null + val sessionId: String? = null, + + //Relation context is in clear in encrypted message + @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? = null ) \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index b55a7e14c9..f8c1024d2a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.room import arrow.core.Try import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.session.crypto.CryptoService +import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.events.model.* import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent 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.* @@ -43,7 +47,9 @@ internal interface EventRelationsAggregationTask : Task + EventAnnotationsSummaryEntity.where(realm, event.eventId + ?: "").findFirst()?.let { + TimelineEventEntity.where(realm, eventId = event.eventId + ?: "").findFirst()?.let { tet -> tet.annotations = it } } } + + EventType.ENCRYPTED -> { + //Relation type is in clear + val encryptedEventContent = event.content.toModel() + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) { + //we need to decrypt if needed + if (event.mxDecryptionResult == null) { + try { + val result = cryptoService.decryptEvent(event, event.roomId) + event.mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + + } + } + event.getClearContent().toModel()?.let { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + //A replace! + handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } + } + } EventType.REDACTION -> { val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } ?: return@forEach @@ -125,9 +159,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private } - private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) { + private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean, relatedEventId: String? = null) { val eventId = event.eventId ?: return - val targetEventId = content.relatesTo?.eventId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return val newContent = content.newContent ?: return //ok, this is a replace var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() 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 index 8ea8c3370d..24dc14a72f 100644 --- 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 @@ -61,13 +61,13 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M } val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId - ?: "").findFirst() - ?: return + ?: "").findFirst() + ?: return val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() - ?: return + ?: return val allowedKeys = computeAllowedKeys(eventToPrune.type) if (allowedKeys.isNotEmpty()) { @@ -75,10 +75,11 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M eventToPrune.content = ContentMapper.map(prunedContent) } else { when (eventToPrune.type) { + EventType.ENCRYPTED, EventType.MESSAGE -> { Timber.d("REDACTION for message ${eventToPrune.eventId}") val unsignedData = EventMapper.map(eventToPrune).unsignedData - ?: UnsignedData(null, null) + ?: UnsignedData(null, null) //was this event a m.replace // val contentModel = ContentMapper.map(eventToPrune.content)?.toModel() @@ -89,6 +90,8 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M val modified = unsignedData.copy(redactedEvent = redactionEvent) eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) + eventToPrune.decryptionResultJson = null + eventToPrune.decryptionErrorCode = null } // EventType.REACTION -> { @@ -112,14 +115,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M 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") + "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") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt index 37a7f09420..025de46279 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/DefaultRelationService.kt @@ -127,9 +127,17 @@ internal class DefaultRelationService @Inject constructor(private val context: C .also { saveLocalEcho(it) } - val workRequest = createSendEventWork(event) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) + if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) + return CancelableWork(context, encryptWork.id) + + } else { + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) + return CancelableWork(context, workRequest.id) + } } @@ -146,13 +154,21 @@ internal class DefaultRelationService @Inject constructor(private val context: C .also { saveLocalEcho(it) } - val workRequest = createSendEventWork(event) - TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) - return CancelableWork(context, workRequest.id) + if (cryptoService.isRoomEncrypted(roomId)) { + val encryptWork = createEncryptEventWork(event, listOf("m.relates_to")) + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postSequentialWorks(context, roomId, encryptWork, workRequest) + return CancelableWork(context, encryptWork.id) + + } else { + val workRequest = createSendEventWork(event) + TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) + return CancelableWork(context, workRequest.id) + } } override fun fetchEditHistory(eventId: String, callback: MatrixCallback>) { - val params = FetchEditHistoryTask.Params(roomId, eventId) + val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId) fetchEditHistoryTask.configureWith(params) .dispatchTo(callback) .executeBy(taskExecutor) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt index e891ece8da..7afbe2884b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/FetchEditHistoryTask.kt @@ -29,6 +29,7 @@ internal interface FetchEditHistoryTask : Task> { return executeRequest { - apiCall = roomAPI.getRelations(params.roomId, params.eventId, RelationType.REPLACE, EventType.MESSAGE) + apiCall = roomAPI.getRelations(params.roomId, + params.eventId, + RelationType.REPLACE, + if (params.isRoomEncrypted) EventType.ENCRYPTED else EventType.MESSAGE) }.map { resp -> - resp.chunks + val events = resp.chunks.toMutableList() + resp.originalEvent?.let { events.add(it) } + events } } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt index 5d39e81c48..737165ff0f 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/relation/RelationsResponse.kt @@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.Event @JsonClass(generateAdapter = true) internal data class RelationsResponse( @Json(name = "chunk") val chunks: List, + @Json(name = "original_event") val originalEvent: Event?, @Json(name = "next_batch") val nextBatch: String?, @Json(name = "prev_batch") val prevBatch: String? ) \ No newline at end of file diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt index 54ff30774d..5b0dbdfed2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageMenuViewModel.kt @@ -244,7 +244,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment if (event.root.getClearType() != EventType.MESSAGE) return false //TODO if user is admin or moderator - val messageContent = event.root.content.toModel() + val messageContent = event.root.getClearContent().toModel() return event.root.senderId == myUserId && ( messageContent?.type == MessageType.MSGTYPE_TEXT || messageContent?.type == MessageType.MSGTYPE_EMOTE diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt index 34e340731f..fc11f25561 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryEpoxyController.kt @@ -75,9 +75,7 @@ class ViewEditHistoryEpoxyController(private val context: Context, } } else { var lastDate: Calendar? = null - sourceEvents.sortedByDescending { - it.originServerTs ?: 0 - }.forEachIndexed { index, timelineEvent -> + sourceEvents.forEachIndexed { index, timelineEvent -> val evDate = Calendar.getInstance().apply { timeInMillis = timelineEvent.originServerTs diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt index 576ef5e91a..cde65463d7 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/ViewEditHistoryViewModel.kt @@ -20,12 +20,15 @@ import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.Session +import im.vector.matrix.android.api.session.crypto.MXCryptoError 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.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.isReply +import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter +import java.util.* data class ViewEditHistoryViewState( @@ -79,16 +82,36 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted } override fun onSuccess(data: List) { - //TODO until supported by API Add original event manually - val withOriginal = data.toMutableList() var originalIsReply = false - room.getTimeLineEvent(eventId)?.let { - withOriginal.add(it.root) - originalIsReply = it.root.getClearContent().toModel().isReply() + + val events = data.map { + val timelineID = it.roomId + UUID.randomUUID().toString() + it.apply { + //We need to check encryption + if (isEncrypted() && mxDecryptionResult == null) { + //for now decrypt sync + try { + val result = session.decryptEvent(this, timelineID) + mxDecryptionResult = OlmDecryptionResult( + payload = result.clearEvent, + senderKey = result.senderCurve25519Key, + keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) }, + forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain + ) + } catch (e: MXCryptoError) { + + } + } + + if (it.eventId == eventId) { + originalIsReply = getClearContent().toModel().isReply() + } + } + } setState { copy( - editList = Success(withOriginal), + editList = Success(events), isOriginalAReply = originalIsReply ) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 42050482bb..3a0d2d1dc5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -27,12 +27,14 @@ import dagger.Lazy import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan import im.vector.matrix.android.api.session.events.model.RelationType +import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.room.model.EditAggregatedSummary import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt +import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.R import im.vector.riotx.core.epoxy.VectorEpoxyModel @@ -83,7 +85,9 @@ class MessageItemFactory @Inject constructor( ?: //Malformed content, we should echo something on screen return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) - if (messageContent.relatesTo?.type == RelationType.REPLACE) { + if (messageContent.relatesTo?.type == RelationType.REPLACE + || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE + ) { // ignore replace event, the targeted id is already edited return BlankItem_() } @@ -229,7 +233,8 @@ class MessageItemFactory @Inject constructor( val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val thumbnailData = ImageContentRenderer.Data( filename = messageContent.body, - url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, + url = messageContent.videoInfo?.thumbnailFile?.url + ?: messageContent.videoInfo?.thumbnailUrl, elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), height = messageContent.videoInfo?.height, maxHeight = maxHeight,