Merge pull request #384 from vector-im/feature/edit_e2e

Feature/edit e2e
This commit is contained in:
Valere 2019-07-18 16:44:44 +02:00 committed by GitHub
commit d87ee32422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 150 additions and 55 deletions

View File

@ -4,6 +4,7 @@ Changes in RiotX 0.2.1 (2019-XX-XX)
Features: Features:
- Message Editing: View edit history (#121) - Message Editing: View edit history (#121)
- Rooms filtering (#304) - Rooms filtering (#304)
- Edit in encrypted room


Improvements: Improvements:
- Handle click on redacted events: view source and create permalink - Handle click on redacted events: view source and create permalink

View File

@ -85,14 +85,12 @@ interface RelationService {
* Edit a reply. This is a special case because replies contains fallback text as a prefix. * Edit a reply. This is a special case because replies contains fallback text as a prefix.
* This method will take the new body (stripped from fallbacks) and re-add them before sending. * This method will take the new body (stripped from fallbacks) and re-add them before sending.
* @param replyToEdit The event to edit * @param replyToEdit The event to edit
* @param originalSenderId the sender of the message that this reply (being edited) is relating to * @param originalTimelineEvent the message that this reply (being edited) is relating to
* @param originalEventId the event id that this reply (being edited) is relating to
* @param newBodyText The edited body (stripped from in reply to content) * @param newBodyText The edited body (stripped from in reply to content)
* @param compatibilityBodyText The text that will appear on clients that don't support yet edition * @param compatibilityBodyText The text that will appear on clients that don't support yet edition
*/ */
fun editReply(replyToEdit: TimelineEvent, fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?, originalTimelineEvent: TimelineEvent,
originalEventId : String,
newBodyText: String, newBodyText: String,
compatibilityBodyText: String = "* $newBodyText"): Cancelable compatibilityBodyText: String = "* $newBodyText"): Cancelable



View File

@ -24,6 +24,7 @@ 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.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent


/** /**
* This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline. * This data class is a wrapper around an Event. It allows to get useful data in the context of a timeline.
@ -94,7 +95,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? = annotations?.editSu


fun TimelineEvent.getTextEditableContent(): String? { fun TimelineEvent.getTextEditableContent(): String? {
val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null val originalContent = root.getClearContent().toModel<MessageContent>() ?: return null
val isReply = originalContent.isReply() val isReply = originalContent.isReply() || root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId != null
val lastContent = getLastMessageContent() val lastContent = getLastMessageContent()
return if (isReply) { return if (isReply) {
return extractUsefulTextFromReply(lastContent?.body ?: "") return extractUsefulTextFromReply(lastContent?.body ?: "")

View File

@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.crypto.model.event


import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent


/** /**
* Class representing an encrypted event content * Class representing an encrypted event content
@ -52,5 +53,8 @@ data class EncryptedEventContent(
* The session id * The session id
*/ */
@Json(name = "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
) )

View File

@ -17,9 +17,13 @@ package im.vector.matrix.android.internal.session.room


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy 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.events.model.*
import im.vector.matrix.android.api.session.room.model.message.MessageContent 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.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.ContentMapper
import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.mapper.EventMapper
import im.vector.matrix.android.internal.database.model.* import im.vector.matrix.android.internal.database.model.*
@ -43,7 +47,9 @@ internal interface EventRelationsAggregationTask : Task<EventRelationsAggregatio
/** /**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/ */
internal class DefaultEventRelationsAggregationTask @Inject constructor(private val monarchy: Monarchy) : EventRelationsAggregationTask { internal class DefaultEventRelationsAggregationTask @Inject constructor(
private val monarchy: Monarchy,
private val cryptoService: CryptoService) : EventRelationsAggregationTask {


//OPT OUT serer aggregation until API mature enough //OPT OUT serer aggregation until API mature enough
private val SHOULD_HANDLE_SERVER_AGREGGATION = false private val SHOULD_HANDLE_SERVER_AGREGGATION = false
@ -86,14 +92,43 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(private
} }
} }


EventAnnotationsSummaryEntity.where(realm, event.eventId ?: "").findFirst()?.let { EventAnnotationsSummaryEntity.where(realm, event.eventId
TimelineEventEntity.where(realm,eventId = event.eventId ?: "").findFirst()?.let { tet -> ?: "").findFirst()?.let {
TimelineEventEntity.where(realm, eventId = event.eventId
?: "").findFirst()?.let { tet ->
tet.annotations = it tet.annotations = it
} }
} }




} }

EventType.ENCRYPTED -> {
//Relation type is in clear
val encryptedEventContent = event.content.toModel<EncryptedEventContent>()
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) {
Timber.w("Failed to decrypt e2e replace")
//TODO -> we should keep track of this and retry, or aggregation will be broken
}
}
event.getClearContent().toModel<MessageContent>()?.let {
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
//A replace!
handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId)
}
}
}
EventType.REDACTION -> { EventType.REDACTION -> {
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() } val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
?: return@forEach ?: return@forEach
@ -125,9 +160,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 eventId = event.eventId ?: return
val targetEventId = content.relatesTo?.eventId ?: return val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return
val newContent = content.newContent ?: return val newContent = content.newContent ?: return
//ok, this is a replace //ok, this is a replace
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()

View File

@ -61,13 +61,13 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
} }


val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
?: "").findFirst() ?: "").findFirst()
?: return ?: return
val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT val isLocalEcho = redactionEventEntity.sendState == SendState.UNSENT
Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho") Timber.v("Redact event for ${redactionEvent.redacts} localEcho=$isLocalEcho")


val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst() val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
?: return ?: return


val allowedKeys = computeAllowedKeys(eventToPrune.type) val allowedKeys = computeAllowedKeys(eventToPrune.type)
if (allowedKeys.isNotEmpty()) { if (allowedKeys.isNotEmpty()) {
@ -75,10 +75,11 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
eventToPrune.content = ContentMapper.map(prunedContent) eventToPrune.content = ContentMapper.map(prunedContent)
} else { } else {
when (eventToPrune.type) { when (eventToPrune.type) {
EventType.ENCRYPTED,
EventType.MESSAGE -> { EventType.MESSAGE -> {
Timber.d("REDACTION for message ${eventToPrune.eventId}") Timber.d("REDACTION for message ${eventToPrune.eventId}")
val unsignedData = EventMapper.map(eventToPrune).unsignedData val unsignedData = EventMapper.map(eventToPrune).unsignedData
?: UnsignedData(null, null) ?: UnsignedData(null, null)


//was this event a m.replace //was this event a m.replace
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>() // val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
@ -89,6 +90,8 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
val modified = unsignedData.copy(redactedEvent = redactionEvent) val modified = unsignedData.copy(redactedEvent = redactionEvent)
eventToPrune.content = ContentMapper.map(emptyMap()) eventToPrune.content = ContentMapper.map(emptyMap())
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified) eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
eventToPrune.decryptionResultJson = null
eventToPrune.decryptionErrorCode = null


} }
// EventType.REACTION -> { // EventType.REACTION -> {
@ -112,14 +115,14 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
EventType.STATE_ROOM_CREATE -> listOf("creator") EventType.STATE_ROOM_CREATE -> listOf("creator")
EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule") EventType.STATE_ROOM_JOIN_RULES -> listOf("join_rule")
EventType.STATE_ROOM_POWER_LEVELS -> listOf("users", EventType.STATE_ROOM_POWER_LEVELS -> listOf("users",
"users_default", "users_default",
"events", "events",
"events_default", "events_default",
"state_default", "state_default",
"ban", "ban",
"kick", "kick",
"redact", "redact",
"invite") "invite")
EventType.STATE_ROOM_ALIASES -> listOf("aliases") EventType.STATE_ROOM_ALIASES -> listOf("aliases")
EventType.STATE_CANONICAL_ALIAS -> listOf("alias") EventType.STATE_CANONICAL_ALIAS -> listOf("alias")
EventType.FEEDBACK -> listOf("type", "target_event_id") EventType.FEEDBACK -> listOf("type", "target_event_id")

View File

@ -127,32 +127,47 @@ internal class DefaultRelationService @Inject constructor(private val context: C
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val workRequest = createSendEventWork(event) if (cryptoService.isRoomEncrypted(roomId)) {
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
return CancelableWork(context, workRequest.id) 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 editReply(replyToEdit: TimelineEvent, override fun editReply(replyToEdit: TimelineEvent,
originalSenderId: String?, originalEvent: TimelineEvent,
originalEventId: String,
newBodyText: String, newBodyText: String,
compatibilityBodyText: String): Cancelable { compatibilityBodyText: String): Cancelable {
val event = eventFactory val event = eventFactory
.createReplaceTextOfReply(roomId, .createReplaceTextOfReply(roomId,
replyToEdit, replyToEdit,
originalSenderId, originalEventId, originalEvent,
newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText) newBodyText, true, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
.also { .also {
saveLocalEcho(it) saveLocalEcho(it)
} }
val workRequest = createSendEventWork(event) if (cryptoService.isRoomEncrypted(roomId)) {
TimelineSendEventWorkCommon.postWork(context, roomId, workRequest) val encryptWork = createEncryptEventWork(event, listOf("m.relates_to"))
return CancelableWork(context, workRequest.id) 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<List<Event>>) { override fun fetchEditHistory(eventId: String, callback: MatrixCallback<List<Event>>) {
val params = FetchEditHistoryTask.Params(roomId, eventId) val params = FetchEditHistoryTask.Params(roomId, cryptoService.isRoomEncrypted(roomId), eventId)
fetchEditHistoryTask.configureWith(params) fetchEditHistoryTask.configureWith(params)
.dispatchTo(callback) .dispatchTo(callback)
.executeBy(taskExecutor) .executeBy(taskExecutor)

View File

@ -29,6 +29,7 @@ internal interface FetchEditHistoryTask : Task<FetchEditHistoryTask.Params, List


data class Params( data class Params(
val roomId: String, val roomId: String,
val isRoomEncrypted: Boolean,
val eventId: String val eventId: String
) )
} }
@ -40,9 +41,14 @@ internal class DefaultFetchEditHistoryTask @Inject constructor(


override suspend fun execute(params: FetchEditHistoryTask.Params): Try<List<Event>> { override suspend fun execute(params: FetchEditHistoryTask.Params): Try<List<Event>> {
return executeRequest<RelationsResponse> { return executeRequest<RelationsResponse> {
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 -> }.map { resp ->
resp.chunks val events = resp.chunks.toMutableList()
resp.originalEvent?.let { events.add(it) }
events
} }
} }
} }

View File

@ -22,6 +22,7 @@ import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class RelationsResponse( internal data class RelationsResponse(
@Json(name = "chunk") val chunks: List<Event>, @Json(name = "chunk") val chunks: List<Event>,
@Json(name = "original_event") val originalEvent: Event?,
@Json(name = "next_batch") val nextBatch: String?, @Json(name = "next_batch") val nextBatch: String?,
@Json(name = "prev_batch") val prevBatch: String? @Json(name = "prev_batch") val prevBatch: String?
) )

View File

@ -105,28 +105,28 @@ internal class LocalEchoEventFactory @Inject constructor(private val credentials
} }


fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent, fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent,
originalSenderId: String?, originalEvent: TimelineEvent,
originalEventId: String,
newBodyText: String, newBodyText: String,
newBodyAutoMarkdown: Boolean, newBodyAutoMarkdown: Boolean,
msgType: String, msgType: String,
compatibilityText: String): Event { compatibilityText: String): Event {
val permalink = PermalinkFactory.createPermalink(roomId, originalEventId) val permalink = PermalinkFactory.createPermalink(roomId, originalEvent.root.eventId ?: "")
val userLink = originalSenderId?.let { PermalinkFactory.createPermalink(it) } ?: "" val userLink = originalEvent.root.senderId?.let { PermalinkFactory.createPermalink(it) }
?: ""


val body = bodyForReply(eventReplaced.getLastMessageContent(), eventReplaced.root.getClearContent().toModel()) val body = bodyForReply(originalEvent.getLastMessageContent(), originalEvent.root.getClearContent().toModel())
val replyFormatted = REPLY_PATTERN.format( val replyFormatted = REPLY_PATTERN.format(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix), stringProvider.getString(R.string.message_reply_to_prefix),
userLink, userLink,
originalSenderId, originalEvent.senderName ?: originalEvent.root.senderId,
body.takeFormatted(), body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
) )
// //
// > <@alice:example.org> This is the original body // > <@alice:example.org> This is the original body
// //
val replyFallback = buildReplyFallback(body, originalSenderId, newBodyText) val replyFallback = buildReplyFallback(body, originalEvent.root.senderId ?: "", newBodyText)


return createEvent(roomId, return createEvent(roomId,
MessageTextContent( MessageTextContent(

View File

@ -38,6 +38,7 @@ import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.message.getFileUrl
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.intent.getFilenameFromUri import im.vector.riotx.core.intent.getFilenameFromUri
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -229,9 +230,12 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro


//is original event a reply? //is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) { if (inReplyTo != null) {
//TODO check if same content? //TODO check if same content?
room.editReply(state.sendMode.timelineEvent, room.getTimeLineEvent(inReplyTo)?.root?.senderId, inReplyTo, action.text) room.getTimeLineEvent(inReplyTo)?.let {
room.editReply(state.sendMode.timelineEvent, it, action.text)
}
} else { } else {
val messageContent: MessageContent? = val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()

View File

@ -244,7 +244,7 @@ class MessageMenuViewModel @AssistedInject constructor(@Assisted initialState: M
//Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment //Only event of type Event.EVENT_TYPE_MESSAGE are supported for the moment
if (event.root.getClearType() != EventType.MESSAGE) return false if (event.root.getClearType() != EventType.MESSAGE) return false
//TODO if user is admin or moderator //TODO if user is admin or moderator
val messageContent = event.root.content.toModel<MessageContent>() val messageContent = event.root.getClearContent().toModel<MessageContent>()
return event.root.senderId == myUserId && ( return event.root.senderId == myUserId && (
messageContent?.type == MessageType.MSGTYPE_TEXT messageContent?.type == MessageType.MSGTYPE_TEXT
|| messageContent?.type == MessageType.MSGTYPE_EMOTE || messageContent?.type == MessageType.MSGTYPE_EMOTE

View File

@ -75,9 +75,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
} else { } else {
var lastDate: Calendar? = null var lastDate: Calendar? = null
sourceEvents.sortedByDescending { sourceEvents.forEachIndexed { index, timelineEvent ->
it.originServerTs ?: 0
}.forEachIndexed { index, timelineEvent ->


val evDate = Calendar.getInstance().apply { val evDate = Calendar.getInstance().apply {
timeInMillis = timelineEvent.originServerTs timeInMillis = timelineEvent.originServerTs

View File

@ -20,12 +20,16 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.Event
import im.vector.matrix.android.api.session.events.model.toModel 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.MessageContent
import im.vector.matrix.android.api.session.room.model.message.isReply 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.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDateFormatter
import timber.log.Timber
import java.util.*




data class ViewEditHistoryViewState( data class ViewEditHistoryViewState(
@ -79,16 +83,36 @@ class ViewEditHistoryViewModel @AssistedInject constructor(@Assisted
} }


override fun onSuccess(data: List<Event>) { override fun onSuccess(data: List<Event>) {
//TODO until supported by API Add original event manually
val withOriginal = data.toMutableList()
var originalIsReply = false var originalIsReply = false
room.getTimeLineEvent(eventId)?.let {
withOriginal.add(it.root) val events = data.map { event ->
originalIsReply = it.root.getClearContent().toModel<MessageContent>().isReply() val timelineID = event.roomId + UUID.randomUUID().toString()
event.also {
//We need to check encryption
if (it.isEncrypted() && it.mxDecryptionResult == null) {
//for now decrypt sync
try {
val result = session.decryptEvent(it, timelineID)
it.mxDecryptionResult = OlmDecryptionResult(
payload = result.clearEvent,
senderKey = result.senderCurve25519Key,
keysClaimed = result.claimedEd25519Key?.let { k -> mapOf("ed25519" to k) },
forwardingCurve25519KeyChain = result.forwardingCurve25519KeyChain
)
} catch (e: MXCryptoError) {
Timber.w("Failed to decrypt event in history")
}
}

if (event.eventId == it.eventId) {
originalIsReply = it.getClearContent().toModel<MessageContent>().isReply()
}
}

} }
setState { setState {
copy( copy(
editList = Success(withOriginal), editList = Success(events),
isOriginalAReply = originalIsReply isOriginalAReply = originalIsReply
) )
} }

View File

@ -27,12 +27,14 @@ import dagger.Lazy
import im.vector.matrix.android.api.permalinks.MatrixLinkify import im.vector.matrix.android.api.permalinks.MatrixLinkify
import im.vector.matrix.android.api.permalinks.MatrixPermalinkSpan 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.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.EditAggregatedSummary
import im.vector.matrix.android.api.session.room.model.message.* 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.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent 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.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt 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.EmojiCompatFontProvider
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -83,7 +85,9 @@ class MessageItemFactory @Inject constructor(
?: //Malformed content, we should echo something on screen ?: //Malformed content, we should echo something on screen
return DefaultItem_().text(stringProvider.getString(R.string.malformed_message)) 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<EncryptedEventContent>()?.relatesTo?.type == RelationType.REPLACE
) {
// ignore replace event, the targeted id is already edited // ignore replace event, the targeted id is already edited
return BlankItem_() return BlankItem_()
} }
@ -229,7 +233,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,