forked from GitHub-Mirror/riotX-android
Merge pull request #384 from vector-im/feature/edit_e2e
Feature/edit e2e
This commit is contained in:
commit
d87ee32422
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 ?: "")
|
||||||
|
@ -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
|
||||||
)
|
)
|
@ -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()
|
||||||
|
@ -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")
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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?
|
||||||
)
|
)
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user