mirror of
https://github.com/element-hq/element-android
synced 2024-11-28 21:48:50 +03:00
Merge pull request #155 from vector-im/feature/aggregation_local_echo
Local echo for reactions/edits/redacts
This commit is contained in:
commit
9357059cbc
27 changed files with 636 additions and 398 deletions
|
@ -48,7 +48,7 @@ android {
|
|||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||
|
||||
// Set to BODY instead of NONE to enable logging
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.NONE"
|
||||
buildConfigField "okhttp3.logging.HttpLoggingInterceptor.Level", "OKHTTP_LOGGING_LEVEL", "okhttp3.logging.HttpLoggingInterceptor.Level.BASIC"
|
||||
}
|
||||
|
||||
release {
|
||||
|
|
|
@ -18,12 +18,10 @@ 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.database.model.SessionRealmModule
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationExtractor
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.membership.SenderRoomMemberExtractor
|
||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||
|
@ -60,8 +58,7 @@ internal class TimelineTest : InstrumentedTest {
|
|||
|
||||
private fun createTimeline(initialEventId: String? = null): Timeline {
|
||||
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
||||
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null))
|
||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau)
|
||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||
|
|
|
@ -21,5 +21,6 @@ data class EditAggregatedSummary(
|
|||
val aggregatedContent: Content? = null,
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
val sourceEvents: List<String>,
|
||||
val localEchos: List<String>,
|
||||
val lastEditTs: Long = 0
|
||||
)
|
||||
|
|
|
@ -5,5 +5,6 @@ data class ReactionAggregatedSummary(
|
|||
val count: Int, // 8
|
||||
val addedByMe: Boolean, // true
|
||||
val firstTimestamp: Long, // unix timestamp
|
||||
val sourceEvents: List<String>
|
||||
val sourceEvents: List<String>,
|
||||
val localEchoEvents: List<String>
|
||||
)
|
|
@ -15,13 +15,15 @@ internal object EventAnnotationsSummaryMapper {
|
|||
it.count,
|
||||
it.addedByMe,
|
||||
it.firstTimestamp,
|
||||
it.sourceEvents.toList()
|
||||
it.sourceEvents.toList(),
|
||||
it.sourceLocalEcho.toList()
|
||||
)
|
||||
},
|
||||
editSummary = annotationsSummary.editSummary?.let {
|
||||
EditAggregatedSummary(
|
||||
ContentMapper.map(it.aggregatedContent),
|
||||
it.sourceEvents.toList(),
|
||||
it.sourceLocalEchoEvents.toList(),
|
||||
it.lastEditTs
|
||||
)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity(
|
|||
var aggregatedContent: String? = null,
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
var sourceEvents: RealmList<String> = RealmList(),
|
||||
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
|
||||
var lastEditTs: Long = 0
|
||||
) : RealmObject() {
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity(
|
|||
// The first time this reaction was added (for ordering purpose)
|
||||
var firstTimestamp: Long = 0,
|
||||
// The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk)
|
||||
var sourceEvents: RealmList<String> = RealmList()
|
||||
var sourceEvents: RealmList<String> = RealmList(),
|
||||
// List of transaction ids for local echos
|
||||
var sourceLocalEcho: RealmList<String> = RealmList()
|
||||
) : RealmObject() {
|
||||
|
||||
companion object
|
||||
|
|
|
@ -48,6 +48,7 @@ internal fun EventEntity.Companion.where(realm: Realm,
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
||||
roomId: String,
|
||||
includedTypes: List<String> = emptyList(),
|
||||
|
|
|
@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||
RoomSummaryUpdater(get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
EventRelationsAggregationUpdater(get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
||||
}
|
||||
|
@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
||||
val userEntityUpdater = UserEntityUpdater(get(), get(), get())
|
||||
listOf<LiveEntityObserver>(groupSummaryUpdater, eventsPruner, userEntityUpdater)
|
||||
val aggregationUpdater = EventRelationsAggregationUpdater(get(), get(), get(), get())
|
||||
//Event pruner must be the last one, because it will clear contents
|
||||
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
||||
listOf<LiveEntityObserver>(groupSummaryUpdater, userEntityUpdater, aggregationUpdater, eventsPruner)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
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.api.session.room.send.SendState
|
||||
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.*
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
|
||||
internal interface EventRelationsAggregationTask : Task<EventRelationsAggregationTask.Params, Unit> {
|
||||
|
||||
data class Params(
|
||||
val events: List<Pair<Event, SendState>>,
|
||||
val userId: String
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
|
||||
*/
|
||||
internal class DefaultEventRelationsAggregationTask(private val monarchy: Monarchy) : EventRelationsAggregationTask {
|
||||
|
||||
override fun execute(params: EventRelationsAggregationTask.Params): Try<Unit> {
|
||||
return monarchy.tryTransactionAsync { realm ->
|
||||
update(realm, params.events, params.userId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun update(realm: Realm, events: List<Pair<Event, SendState>>, userId: String) {
|
||||
events.forEach { pair ->
|
||||
val roomId = pair.first.roomId ?: return@forEach
|
||||
val event = pair.first
|
||||
val sendState = pair.second
|
||||
val isLocalEcho = sendState == SendState.UNSENT
|
||||
when (event.type) {
|
||||
EventType.REACTION -> {
|
||||
//we got a reaction!!
|
||||
Timber.v("###REACTION in room $roomId")
|
||||
handleReaction(event, roomId, realm, userId, isLocalEcho)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||
} else {
|
||||
val content: MessageContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
//A replace!
|
||||
handleReplace(realm, event, content, roomId, isLocalEcho)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
EventType.REDACTION -> {
|
||||
val eventToPrune = event.redacts?.let { EventEntity.where(realm, eventId = it).findFirst() }
|
||||
?: return
|
||||
when (eventToPrune.type) {
|
||||
EventType.MESSAGE -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
?: UnsignedData(null, null)
|
||||
|
||||
//was this event a m.replace
|
||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
}
|
||||
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
handleReactionRedact(eventToPrune, realm, userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReplace(realm: Realm, event: Event, content: MessageContent, roomId: String, isLocalEcho: Boolean) {
|
||||
val eventId = event.eventId ?: return
|
||||
val targetEventId = content.relatesTo?.eventId ?: return
|
||||
val newContent = content.newContent ?: return
|
||||
//ok, this is a replace
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||
if (existing == null) {
|
||||
Timber.v("###REPLACE creating no relation summary for ${targetEventId}")
|
||||
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
|
||||
existing.roomId = roomId
|
||||
}
|
||||
|
||||
//we have it
|
||||
val existingSummary = existing.editSummary
|
||||
if (existingSummary == null) {
|
||||
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one (localEcho:$isLocalEcho)")
|
||||
//create the edit summary
|
||||
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
||||
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
if (isLocalEcho) {
|
||||
editSummary.sourceLocalEchoEvents.add(eventId)
|
||||
} else {
|
||||
editSummary.sourceEvents.add(eventId)
|
||||
}
|
||||
|
||||
existing.editSummary = editSummary
|
||||
} else {
|
||||
if (existingSummary.sourceEvents.contains(eventId)) {
|
||||
//ignore this event, we already know it (??)
|
||||
Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}")
|
||||
return
|
||||
}
|
||||
val txId = event.unsignedData?.transactionId
|
||||
//is it a remote echo?
|
||||
if (!isLocalEcho && existingSummary.sourceLocalEchoEvents.contains(txId)) {
|
||||
//ok it has already been managed
|
||||
Timber.v("###REPLACE Receiving remote echo of edit (edit already done)")
|
||||
existingSummary.sourceLocalEchoEvents.remove(txId)
|
||||
existingSummary.sourceEvents.add(event.eventId)
|
||||
} else if (event.originServerTs ?: 0 > existingSummary.lastEditTs) {
|
||||
Timber.v("###REPLACE Computing aggregated edit summary (isLocalEcho:$isLocalEcho)")
|
||||
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
existingSummary.sourceEvents.add(eventId)
|
||||
} else {
|
||||
//ignore this event for the summary
|
||||
Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||
aggregation.chunk?.forEach {
|
||||
if (it.type == EventType.REACTION) {
|
||||
val eventId = event.eventId ?: ""
|
||||
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
if (existing == null) {
|
||||
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
|
||||
eventSummary.roomId = roomId
|
||||
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||
sum.key = it.key
|
||||
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
|
||||
sum.count = it.count
|
||||
eventSummary.reactionsSummary.add(sum)
|
||||
} else {
|
||||
//TODO how to handle that
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReaction(event: Event, roomId: String, realm: Realm, userId: String, isLocalEcho: Boolean) {
|
||||
event.content.toModel<ReactionContent>()?.let { content ->
|
||||
//rel_type must be m.annotation
|
||||
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
val eventId = content.relatesTo.eventId
|
||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId }
|
||||
|
||||
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||
val txId = event.unsignedData?.transactionId
|
||||
if (isLocalEcho && txId.isNullOrBlank()) {
|
||||
Timber.w("Received a local echo with no transaction ID")
|
||||
}
|
||||
if (sum == null) {
|
||||
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||
sum.key = reaction
|
||||
sum.firstTimestamp = event.originServerTs ?: 0
|
||||
if (isLocalEcho) {
|
||||
Timber.v("Adding local echo reaction $reaction")
|
||||
sum.sourceLocalEcho.add(txId)
|
||||
sum.count = 1
|
||||
} else {
|
||||
Timber.v("Adding synced reaction $reaction")
|
||||
sum.count = 1
|
||||
sum.sourceEvents.add(event.eventId)
|
||||
}
|
||||
sum.addedByMe = sum.addedByMe || (userId == event.sender)
|
||||
eventSummary.reactionsSummary.add(sum)
|
||||
} else {
|
||||
//is this a known event (is possible? pagination?)
|
||||
if (!sum.sourceEvents.contains(eventId)) {
|
||||
|
||||
//check if it's not the sync of a local echo
|
||||
if (!isLocalEcho && sum.sourceLocalEcho.contains(txId)) {
|
||||
//ok it has already been counted, just sync the list, do not touch count
|
||||
Timber.v("Ignoring synced of local echo for reaction $reaction")
|
||||
sum.sourceLocalEcho.remove(txId)
|
||||
sum.sourceEvents.add(event.eventId)
|
||||
} else {
|
||||
sum.count += 1
|
||||
if (isLocalEcho) {
|
||||
Timber.v("Adding local echo reaction $reaction")
|
||||
sum.sourceLocalEcho.add(txId)
|
||||
} else {
|
||||
Timber.v("Adding synced reaction $reaction")
|
||||
sum.sourceEvents.add(event.eventId)
|
||||
}
|
||||
|
||||
sum.addedByMe = sum.addedByMe || (userId == event.sender)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an event is deleted
|
||||
*/
|
||||
private fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
||||
Timber.d("Handle redaction of m.replace")
|
||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
||||
if (eventSummary == null) {
|
||||
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
||||
return
|
||||
}
|
||||
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
||||
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
||||
if (sourceToDiscard == null) {
|
||||
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||
return
|
||||
}
|
||||
//Need to remove this event from the redaction list and compute new aggregation state
|
||||
sourceEvents.removeAt(sourceToDiscard)
|
||||
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
||||
if (previousEdit == null) {
|
||||
//revert to original
|
||||
eventSummary.editSummary?.deleteFromRealm()
|
||||
} else {
|
||||
//I have the last event
|
||||
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
||||
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
||||
?: System.currentTimeMillis()
|
||||
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
||||
} ?: run {
|
||||
Timber.e("Failed to udate edited summary")
|
||||
//TODO how to reccover that
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
||||
Timber.v("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.v("REMOVE reaction for key $reactionKey")
|
||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
||||
if (summary != null) {
|
||||
summary.reactionsSummary.where()
|
||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionKey)
|
||||
.findFirst()?.let { aggregation ->
|
||||
Timber.v("Find summary for key with ${aggregation.sourceEvents.size} known reactions (count:${aggregation.count})")
|
||||
Timber.v("Known reactions ${aggregation.sourceEvents.joinToString(",")}")
|
||||
if (aggregation.sourceEvents.contains(eventToPrune.eventId)) {
|
||||
Timber.v("REMOVE reaction for key $reactionKey")
|
||||
aggregation.sourceEvents.remove(eventToPrune.eventId)
|
||||
Timber.v("Known reactions after ${aggregation.sourceEvents.joinToString(",")}")
|
||||
aggregation.count = aggregation.count - 1
|
||||
if (eventToPrune.sender == userId) {
|
||||
//Was it a redact on my reaction?
|
||||
aggregation.addedByMe = false
|
||||
}
|
||||
if (aggregation.count == 0) {
|
||||
//delete!
|
||||
aggregation.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")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,16 +15,14 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
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.*
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import io.realm.Realm
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
|
@ -32,198 +30,34 @@ import timber.log.Timber
|
|||
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
||||
* The summaries can then be extracted and added (as a decoration) to a TimelineEvent for final display.
|
||||
*/
|
||||
internal class EventRelationsAggregationUpdater(private val credentials: Credentials) {
|
||||
internal class EventRelationsAggregationUpdater(monarchy: Monarchy,
|
||||
private val credentials: Credentials,
|
||||
private val task: EventRelationsAggregationTask,
|
||||
private val taskExecutor: TaskExecutor) :
|
||||
RealmLiveEntityObserver<EventEntity>(monarchy) {
|
||||
|
||||
fun update(realm: Realm, roomId: String, events: List<Event>?) {
|
||||
events?.forEach { event ->
|
||||
when (event.type) {
|
||||
EventType.REACTION -> {
|
||||
//we got a reaction!!
|
||||
Timber.v("###REACTION in room $roomId")
|
||||
handleReaction(event, roomId, realm)
|
||||
}
|
||||
EventType.MESSAGE -> {
|
||||
if (event.unsignedData?.relations?.annotations != null) {
|
||||
Timber.v("###REACTION Agreggation in room $roomId for event ${event.eventId}")
|
||||
handleInitialAggregatedRelations(event, roomId, event.unsignedData.relations.annotations, realm)
|
||||
} else {
|
||||
val content: MessageContent? = event.content.toModel()
|
||||
if (content?.relatesTo?.type == RelationType.REPLACE) {
|
||||
Timber.v("###REPLACE in room $roomId for event ${event.eventId}")
|
||||
//A replace!
|
||||
handleReplace(event, content, roomId, realm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
override val query = Monarchy.Query<EventEntity> {
|
||||
EventEntity.where(it)
|
||||
//mmm why is this query not working?
|
||||
// EventEntity.byTypes(it, listOf(
|
||||
// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION)
|
||||
// )
|
||||
}
|
||||
|
||||
private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) {
|
||||
val eventId = event.eventId ?: return
|
||||
val targetEventId = content.relatesTo?.eventId ?: return
|
||||
val newContent = content.newContent ?: return
|
||||
//ok, this is a replace
|
||||
var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst()
|
||||
if (existing == null) {
|
||||
Timber.v("###REPLACE creating no relation summary for ${targetEventId}")
|
||||
existing = EventAnnotationsSummaryEntity.create(realm, targetEventId)
|
||||
existing.roomId = roomId
|
||||
}
|
||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
|
||||
val inserted = inserted
|
||||
.mapNotNull { it.asDomain() to it.sendState }
|
||||
|
||||
//we have it
|
||||
val existingSummary = existing.editSummary
|
||||
if (existingSummary == null) {
|
||||
Timber.v("###REPLACE no edit summary for ${targetEventId}, creating one")
|
||||
//create the edit summary
|
||||
val editSummary = realm.createObject(EditAggregatedSummaryEntity::class.java)
|
||||
editSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
editSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
editSummary.sourceEvents.add(eventId)
|
||||
val params = EventRelationsAggregationTask.Params(
|
||||
inserted,
|
||||
credentials.userId
|
||||
)
|
||||
|
||||
existing.editSummary = editSummary
|
||||
} else {
|
||||
if (existingSummary.sourceEvents.contains(eventId)) {
|
||||
//ignore this event, we already know it (??)
|
||||
Timber.v("###REPLACE ignoring event for summary, it's known ${eventId}")
|
||||
return
|
||||
}
|
||||
//This message has already been edited
|
||||
if (event.originServerTs ?: 0 > existingSummary.lastEditTs ?: 0) {
|
||||
Timber.v("###REPLACE Computing aggregated edit summary")
|
||||
existingSummary.lastEditTs = event.originServerTs ?: System.currentTimeMillis()
|
||||
existingSummary.aggregatedContent = ContentMapper.map(newContent)
|
||||
existingSummary.sourceEvents.add(eventId)
|
||||
} else {
|
||||
//ignore this event for the summary
|
||||
Timber.v("###REPLACE ignoring event for summary, it's to old ${eventId}")
|
||||
}
|
||||
}
|
||||
task.configureWith(params)
|
||||
.executeBy(taskExecutor)
|
||||
|
||||
}
|
||||
|
||||
private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) {
|
||||
aggregation.chunk?.forEach {
|
||||
if (it.type == EventType.REACTION) {
|
||||
val eventId = event.eventId ?: ""
|
||||
val existing = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
if (existing == null) {
|
||||
val eventSummary = EventAnnotationsSummaryEntity.create(realm, eventId)
|
||||
eventSummary.roomId = roomId
|
||||
val sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||
sum.key = it.key
|
||||
sum.firstTimestamp = event.originServerTs ?: 0 //TODO how to maintain order?
|
||||
sum.count = it.count
|
||||
eventSummary.reactionsSummary.add(sum)
|
||||
} else {
|
||||
//TODO how to handle that
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleReaction(event: Event, roomId: String, realm: Realm) {
|
||||
event.content.toModel<ReactionContent>()?.let { content ->
|
||||
//rel_type must be m.annotation
|
||||
if (RelationType.ANNOTATION == content.relatesTo?.type) {
|
||||
val reaction = content.relatesTo.key
|
||||
val eventId = content.relatesTo.eventId
|
||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
?: EventAnnotationsSummaryEntity.create(realm, eventId).apply { this.roomId = roomId }
|
||||
|
||||
var sum = eventSummary.reactionsSummary.find { it.key == reaction }
|
||||
if (sum == null) {
|
||||
sum = realm.createObject(ReactionAggregatedSummaryEntity::class.java)
|
||||
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(event.eventId)
|
||||
sum.addedByMe = sum.addedByMe || (credentials.userId == event.sender)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an event is deleted
|
||||
*/
|
||||
fun handleRedactionOfReplace(redacted: EventEntity, relatedEventId: String, realm: Realm) {
|
||||
Timber.d("Handle redaction of m.replace")
|
||||
val eventSummary = EventAnnotationsSummaryEntity.where(realm, relatedEventId).findFirst()
|
||||
if (eventSummary == null) {
|
||||
Timber.w("Redaction of a replace targeting an unknown event $relatedEventId")
|
||||
return
|
||||
}
|
||||
val sourceEvents = eventSummary.editSummary?.sourceEvents
|
||||
val sourceToDiscard = sourceEvents?.indexOf(redacted.eventId)
|
||||
if (sourceToDiscard == null) {
|
||||
Timber.w("Redaction of a replace that was not known in aggregation $sourceToDiscard")
|
||||
return
|
||||
}
|
||||
//Need to remove this event from the redaction list and compute new aggregation state
|
||||
sourceEvents.removeAt(sourceToDiscard)
|
||||
val previousEdit = sourceEvents.mapNotNull { EventEntity.where(realm, it).findFirst() }.sortedBy { it.originServerTs }.lastOrNull()
|
||||
if (previousEdit == null) {
|
||||
//revert to original
|
||||
eventSummary.editSummary?.deleteFromRealm()
|
||||
} else {
|
||||
//I have the last event
|
||||
ContentMapper.map(previousEdit.content)?.toModel<MessageContent>()?.newContent?.let { newContent ->
|
||||
eventSummary.editSummary?.lastEditTs = previousEdit.originServerTs
|
||||
?: System.currentTimeMillis()
|
||||
eventSummary.editSummary?.aggregatedContent = ContentMapper.map(newContent)
|
||||
} ?: run {
|
||||
Timber.e("Failed to udate edited summary")
|
||||
//TODO how to reccover that
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun handleReactionRedact(eventToPrune: EventEntity, realm: Realm, userId: String) {
|
||||
Timber.d("REDACTION of reaction ${eventToPrune.eventId}")
|
||||
//delete a reaction, need to update the annotation summary if any
|
||||
val reactionContent: ReactionContent = EventMapper.map(eventToPrune).content.toModel()
|
||||
?: return
|
||||
val eventThatWasReacted = reactionContent.relatesTo?.eventId ?: return
|
||||
|
||||
val reactionkey = reactionContent.relatesTo.key
|
||||
Timber.d("REMOVE reaction for key $reactionkey")
|
||||
val summary = EventAnnotationsSummaryEntity.where(realm, eventThatWasReacted).findFirst()
|
||||
if (summary != null) {
|
||||
summary.reactionsSummary.where()
|
||||
.equalTo(ReactionAggregatedSummaryEntityFields.KEY, reactionkey)
|
||||
.findFirst()?.let { summary ->
|
||||
Timber.d("Find summary for key with ${summary.sourceEvents.size} known reactions (count:${summary.count})")
|
||||
Timber.d("Known reactions ${summary.sourceEvents.joinToString(",")}")
|
||||
if (summary.sourceEvents.contains(eventToPrune.eventId)) {
|
||||
Timber.d("REMOVE reaction for key $reactionkey")
|
||||
summary.sourceEvents.remove(eventToPrune.eventId)
|
||||
Timber.d("Known reactions after ${summary.sourceEvents.joinToString(",")}")
|
||||
summary.count = summary.count - 1
|
||||
if (eventToPrune.sender == userId) {
|
||||
//Was it a redact on my reaction?
|
||||
summary.addedByMe = false
|
||||
}
|
||||
if (summary.count == 0) {
|
||||
//delete!
|
||||
summary.deleteFromRealm()
|
||||
}
|
||||
} else {
|
||||
Timber.e("## Cannot remove summary from count, corresponding reaction ${eventToPrune.eventId} is not known")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.e("## Cannot find summary for key $reactionkey")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,10 +17,6 @@
|
|||
package im.vector.matrix.android.internal.session.room
|
||||
|
||||
import im.vector.matrix.android.internal.session.DefaultSession
|
||||
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
|
||||
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
||||
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
|
||||
|
@ -35,6 +31,10 @@ import im.vector.matrix.android.internal.session.room.prune.DefaultPruneEventTas
|
|||
import im.vector.matrix.android.internal.session.room.prune.PruneEventTask
|
||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.DefaultFindReactionEventForUndoTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.DefaultUpdateQuickReactionTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.FindReactionEventForUndoTask
|
||||
import im.vector.matrix.android.internal.session.room.relation.UpdateQuickReactionTask
|
||||
import im.vector.matrix.android.internal.session.room.send.LocalEchoEventFactory
|
||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||
|
@ -57,7 +57,7 @@ class RoomModule {
|
|||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
TokenChunkEventPersistor(get(), get())
|
||||
TokenChunkEventPersistor(get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
|
@ -109,7 +109,11 @@ class RoomModule {
|
|||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultPruneEventTask(get(), get()) as PruneEventTask
|
||||
DefaultPruneEventTask(get()) as PruneEventTask
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
DefaultEventRelationsAggregationTask(get()) as EventRelationsAggregationTask
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,8 +25,12 @@ import im.vector.matrix.android.internal.database.model.EventEntity
|
|||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import timber.log.Timber
|
||||
|
||||
|
||||
/**
|
||||
* Listens to the database for the insertion of any redaction event.
|
||||
* As it will actually delete the content, it should be called last in the list of listener.
|
||||
*/
|
||||
internal class EventsPruner(monarchy: Monarchy,
|
||||
private val credentials: Credentials,
|
||||
private val pruneEventTask: PruneEventTask,
|
||||
|
@ -36,6 +40,7 @@ internal class EventsPruner(monarchy: Monarchy,
|
|||
override val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
||||
|
||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||
Timber.v("Event pruner called with ${inserted.size} insertions")
|
||||
val redactionEvents = inserted
|
||||
.mapNotNull { it.asDomain() }
|
||||
|
||||
|
|
|
@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune
|
|||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
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.room.send.SendState
|
||||
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.EventEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.Realm
|
||||
|
@ -41,8 +42,7 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
|||
}
|
||||
|
||||
internal class DefaultPruneEventTask(
|
||||
private val monarchy: Monarchy,
|
||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask {
|
||||
private val monarchy: Monarchy) : PruneEventTask {
|
||||
|
||||
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
||||
return monarchy.tryTransactionSync { realm ->
|
||||
|
@ -57,6 +57,12 @@ internal class DefaultPruneEventTask(
|
|||
return
|
||||
}
|
||||
|
||||
val redactionEventEntity = EventEntity.where(realm, eventId = redactionEvent.eventId
|
||||
?: "").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
|
||||
|
||||
|
@ -72,19 +78,19 @@ internal class DefaultPruneEventTask(
|
|||
?: UnsignedData(null, null)
|
||||
|
||||
//was this event a m.replace
|
||||
val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
}
|
||||
// val contentModel = ContentMapper.map(eventToPrune.content)?.toModel<MessageContent>()
|
||||
// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||
// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||
// }
|
||||
|
||||
val modified = unsignedData.copy(redactedEvent = redactionEvent)
|
||||
eventToPrune.content = ContentMapper.map(emptyMap())
|
||||
eventToPrune.unsignedData = MoshiProvider.providesMoshi().adapter(UnsignedData::class.java).toJson(modified)
|
||||
|
||||
}
|
||||
EventType.REACTION -> {
|
||||
eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
||||
}
|
||||
// EventType.REACTION -> {
|
||||
// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
*/
|
||||
package im.vector.matrix.android.internal.session.room.relation
|
||||
|
||||
import androidx.work.*
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
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.relation.RelationService
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationService
|
||||
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
|
||||
|
@ -30,19 +30,14 @@ 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.session.room.timeline.TimelineSendEventWorkCommon
|
||||
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
|
||||
import timber.log.Timber
|
||||
|
||||
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 DefaultRelationService(private val roomId: String,
|
||||
private val eventFactory: LocalEchoEventFactory,
|
||||
|
@ -55,28 +50,22 @@ internal class DefaultRelationService(private val roomId: String,
|
|||
|
||||
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
|
||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||
// .also {
|
||||
// //saveLocalEcho(it)
|
||||
// }
|
||||
.also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val sendRelationWork = createSendRelationWork(event)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork)
|
||||
.enqueue()
|
||||
TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork)
|
||||
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<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||
|
||||
}
|
||||
|
||||
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
||||
|
@ -91,11 +80,19 @@ internal class DefaultRelationService(private val roomId: String,
|
|||
.enableRetry()
|
||||
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
||||
override fun onSuccess(data: FindReactionEventForUndoTask.Result) {
|
||||
if (data.redactEventId == null) {
|
||||
Timber.w("Cannot find reaction to undo (not yet synced?)")
|
||||
//TODO?
|
||||
}
|
||||
data.redactEventId?.let { toRedact ->
|
||||
val redactWork = createRedactEventWork(toRedact, null)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
||||
.enqueue()
|
||||
|
||||
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val redactWork = createRedactEventWork(redactEvent, toRedact, null)
|
||||
|
||||
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -119,10 +116,11 @@ internal class DefaultRelationService(private val roomId: String,
|
|||
override fun onSuccess(data: UpdateQuickReactionTask.Result) {
|
||||
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
|
||||
data.reactionToRedact.forEach {
|
||||
val redactWork = createRedactEventWork(it, null)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
||||
.enqueue()
|
||||
val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val redactWork = createRedactEventWork(redactEvent, it, null)
|
||||
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -133,48 +131,26 @@ internal class DefaultRelationService(private val roomId: 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 {
|
||||
private fun createRedactEventWork(localEvent: Event, eventId: String, reason: String?): OneTimeWorkRequest {
|
||||
|
||||
//TODO create local echo of m.room.redaction event?
|
||||
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(localEvent.eventId!!,
|
||||
roomId, eventId, reason)
|
||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(redactWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||
}
|
||||
|
||||
override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
|
||||
val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText)
|
||||
val event = eventFactory.createReplaceTextEvent(roomId, targetEventId, newBodyText, newBodyAutoMarkdown, MessageType.MSGTYPE_TEXT, compatibilityBodyText).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
//TODO use relation API?
|
||||
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue()
|
||||
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||
return CancelableWork(workRequest.id)
|
||||
|
||||
}
|
||||
|
@ -186,18 +162,19 @@ internal class DefaultRelationService(private val roomId: String,
|
|||
} ?: return null
|
||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue()
|
||||
|
||||
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||
return CancelableWork(workRequest.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the event in database as a local echo.
|
||||
* SendState is set to UNSENT and it's added to a the sendingTimelineEvents list of the room.
|
||||
* The sendingTimelineEvents is checked on new sync and will remove the local echo if an event with
|
||||
* the same transaction id is received (in unsigned data)
|
||||
*/
|
||||
private fun saveLocalEcho(event: Event) {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||
|
|
|
@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
|
|||
return result.fold({
|
||||
when (it) {
|
||||
is Failure.NetworkConnection -> Result.retry()
|
||||
else -> Result.failure()
|
||||
else -> {
|
||||
//TODO mark as failed to send?
|
||||
//always return success, or the chain will be stuck for ever!
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
}, { Result.success() })
|
||||
}
|
||||
|
|
|
@ -24,18 +24,12 @@ import im.vector.matrix.android.api.session.room.send.SendService
|
|||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.api.util.CancelableBag
|
||||
import im.vector.matrix.android.api.util.addTo
|
||||
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.content.UploadContentWorker
|
||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineSendEventWorkCommon
|
||||
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 SEND_WORK = "SEND_WORK"
|
||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
|
||||
|
@ -54,9 +48,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||
saveLocalEcho(it)
|
||||
}
|
||||
val sendWork = createSendEventWork(event)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
||||
.enqueue()
|
||||
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
|
@ -65,9 +57,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||
saveLocalEcho(it)
|
||||
}
|
||||
val sendWork = createSendEventWork(event)
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
||||
.enqueue()
|
||||
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||
return CancelableWork(sendWork.id)
|
||||
}
|
||||
|
||||
|
@ -80,12 +70,9 @@ internal class DefaultSendService(private val roomId: String,
|
|||
}
|
||||
|
||||
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, redactWork)
|
||||
.enqueue()
|
||||
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||
return CancelableWork(redactWork.id)
|
||||
}
|
||||
|
||||
|
@ -106,14 +93,7 @@ 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
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = roomId)
|
||||
?: return@tryTransactionAsync
|
||||
|
||||
roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
||||
}
|
||||
eventFactory.saveLocalEcho(monarchy, event)
|
||||
}
|
||||
|
||||
private fun buildWorkIdentifier(identifier: String): String {
|
||||
|
@ -124,26 +104,20 @@ internal class DefaultSendService(private val roomId: String,
|
|||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(sendWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||
}
|
||||
|
||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||
|
||||
//TODO create local echo of m.room.redaction event?
|
||||
val redactEvent = eventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
||||
saveLocalEcho(it)
|
||||
}
|
||||
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
||||
roomId, event.eventId!!, reason)
|
||||
val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!,
|
||||
roomId, event.eventId, reason)
|
||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||
|
||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(redactWorkData)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||
}
|
||||
|
||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
||||
|
|
|
@ -17,22 +17,38 @@
|
|||
package im.vector.matrix.android.internal.session.room.send
|
||||
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.text.TextUtils
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.auth.data.Credentials
|
||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
||||
import im.vector.matrix.android.api.session.events.model.*
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
||||
import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent
|
||||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
||||
import im.vector.matrix.android.api.session.room.model.message.*
|
||||
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.content.ThumbnailExtractor
|
||||
import im.vector.matrix.android.internal.util.StringProvider
|
||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||
import org.commonmark.parser.Parser
|
||||
import org.commonmark.renderer.html.HtmlRenderer
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Creates local echo of events for room events.
|
||||
* A local echo is an event that is persisted even if not yet sent to the server,
|
||||
* in an optimistic way (as if the server as responded immediately). Local echo are using a local id,
|
||||
* (the transaction ID), this id is used when receiving an event from a sync to check if this event
|
||||
* is matching an existing local echo.
|
||||
*
|
||||
* The transactionID is used as loc
|
||||
*/
|
||||
internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {
|
||||
|
||||
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
|
||||
|
@ -41,7 +57,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
val document = parser.parse(text)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (!TextUtils.equals(text, htmlText)) {
|
||||
if (isFormattedTextPertinent(text, htmlText)) { //FIXME
|
||||
return createFormattedTextEvent(roomId, text, htmlText)
|
||||
}
|
||||
}
|
||||
|
@ -49,6 +65,9 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
return createEvent(roomId, content)
|
||||
}
|
||||
|
||||
private fun isFormattedTextPertinent(text: String, htmlText: String?) =
|
||||
text != htmlText && htmlText != "<p>$text</p>\n"
|
||||
|
||||
fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
|
||||
val content = MessageTextContent(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
|
@ -71,7 +90,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
val document = parser.parse(newBodyText)
|
||||
val renderer = HtmlRenderer.builder().build()
|
||||
val htmlText = renderer.render(document)
|
||||
if (!TextUtils.equals(newBodyText, htmlText)) {
|
||||
if (isFormattedTextPertinent(newBodyText, htmlText)) {
|
||||
newContent = MessageTextContent(
|
||||
type = MessageType.MSGTYPE_TEXT,
|
||||
format = MessageType.FORMAT_MATRIX_HTML,
|
||||
|
@ -95,7 +114,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
ContentAttachmentData.Type.IMAGE -> createImageEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.VIDEO -> createVideoEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.AUDIO -> createAudioEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
|
||||
ContentAttachmentData.Type.FILE -> createFileEvent(roomId, attachment)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
reaction
|
||||
)
|
||||
)
|
||||
val localId = dummyEventId(roomId)
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
sender = credentials.userId,
|
||||
eventId = dummyEventId(roomId),
|
||||
eventId = localId,
|
||||
type = EventType.REACTION,
|
||||
content = content.toContent()
|
||||
)
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localId))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -196,13 +217,15 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
}
|
||||
|
||||
private fun createEvent(roomId: String, content: Any? = null): Event {
|
||||
val localID = dummyEventId(roomId)
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
sender = credentials.userId,
|
||||
eventId = dummyEventId(roomId),
|
||||
eventId = localID,
|
||||
type = EventType.MESSAGE,
|
||||
content = content.toContent()
|
||||
content = content.toContent(),
|
||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -211,7 +234,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
}
|
||||
|
||||
private fun dummyEventId(roomId: String): String {
|
||||
return roomId + "-" + dummyOriginServerTs()
|
||||
return "m.${UUID.randomUUID()}"
|
||||
}
|
||||
|
||||
fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
|
||||
|
@ -276,13 +299,55 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||
}
|
||||
return content.body to formattedText
|
||||
}
|
||||
MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null
|
||||
MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null
|
||||
MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null
|
||||
MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null
|
||||
else -> return (content?.body ?: "") to null
|
||||
MessageType.MSGTYPE_FILE -> return stringProvider.getString(R.string.reply_to_a_file) to null
|
||||
MessageType.MSGTYPE_AUDIO -> return stringProvider.getString(R.string.reply_to_an_audio_file) to null
|
||||
MessageType.MSGTYPE_IMAGE -> return stringProvider.getString(R.string.reply_to_an_image) to null
|
||||
MessageType.MSGTYPE_VIDEO -> return stringProvider.getString(R.string.reply_to_a_video) to null
|
||||
else -> return (content?.body ?: "") to null
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* {
|
||||
"content": {
|
||||
"reason": "Spamming"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:domain.com",
|
||||
"origin_server_ts": 1432735824653,
|
||||
"redacts": "$fukweghifu23:localhost",
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:domain.com",
|
||||
"sender": "@example:domain.com",
|
||||
"type": "m.room.redaction",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
}
|
||||
*/
|
||||
fun createRedactEvent(roomId: String, eventId: String, reason: String?): Event {
|
||||
val localID = dummyEventId(roomId)
|
||||
return Event(
|
||||
roomId = roomId,
|
||||
originServerTs = dummyOriginServerTs(),
|
||||
sender = credentials.userId,
|
||||
eventId = localID,
|
||||
type = EventType.REDACTION,
|
||||
redacts = eventId,
|
||||
content = reason?.let { mapOf("reason" to it).toContent() },
|
||||
unsignedData = UnsignedData(age = null, transactionId = localID)
|
||||
)
|
||||
}
|
||||
|
||||
fun saveLocalEcho(monarchy: Monarchy, event: Event) {
|
||||
monarchy.tryTransactionAsync { realm ->
|
||||
val roomEntity = RoomEntity.where(realm, roomId = event.roomId!!).findFirst()
|
||||
?: return@tryTransactionAsync
|
||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId = event.roomId)
|
||||
?: return@tryTransactionAsync
|
||||
|
||||
roomEntity.addSendingEvent(event, liveChunk.forwardsStateIndex ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -25,13 +25,13 @@ 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 txID: String,
|
||||
val roomId: String,
|
||||
val eventId: String,
|
||||
val reason: String?
|
||||
|
@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
|||
private val roomAPI by inject<RoomAPI>()
|
||||
|
||||
override fun doWork(): Result {
|
||||
val params = WorkerParamsFactory.fromData<RedactEventWorker.Params>(inputData)
|
||||
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||
?: return Result.failure()
|
||||
|
||||
if (params.eventId == null) {
|
||||
return Result.failure()
|
||||
}
|
||||
val txID = UUID.randomUUID().toString()
|
||||
|
||||
val eventId = params.eventId
|
||||
val result = executeRequest<SendResponse> {
|
||||
apiCall = roomAPI.redactEvent(
|
||||
txID,
|
||||
params.txID,
|
||||
params.roomId,
|
||||
params.eventId,
|
||||
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()
|
||||
else -> {
|
||||
//TODO mark as failed to send?
|
||||
//always return success, or the chain will be stuck for ever!
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
}, {
|
||||
Result.success()
|
||||
|
|
|
@ -61,7 +61,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
|
|||
return result.fold({
|
||||
when (it) {
|
||||
is Failure.NetworkConnection -> Result.retry()
|
||||
else -> Result.failure()
|
||||
else -> {
|
||||
//TODO mark as failed to send?
|
||||
//always return success, or the chain will be stuck for ever!
|
||||
Result.success()
|
||||
}
|
||||
}
|
||||
}, { Result.success() })
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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.timeline
|
||||
|
||||
import androidx.work.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
private const val SEND_WORK = "SEND_WORK"
|
||||
private const val BACKOFF_DELAY = 10_000L
|
||||
|
||||
private val WORK_CONSTRAINTS = Constraints.Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Helper class for sending event related works.
|
||||
* All send event from a room are using the same workchain, in order to ensure order.
|
||||
* WorkRequest must always return success (even if server error, in this case marking the event as failed to send)
|
||||
* , if not the chain will be doomed in failed state.
|
||||
*
|
||||
*/
|
||||
internal object TimelineSendEventWorkCommon {
|
||||
|
||||
fun postWork(roomId: String, workRequest: OneTimeWorkRequest) {
|
||||
WorkManager.getInstance()
|
||||
.beginUniqueWork(buildWorkIdentifier(roomId), ExistingWorkPolicy.APPEND, workRequest)
|
||||
.enqueue()
|
||||
|
||||
}
|
||||
|
||||
inline fun <reified W : ListenableWorker> createWork(data: Data): OneTimeWorkRequest {
|
||||
return OneTimeWorkRequestBuilder<W>()
|
||||
.setConstraints(WORK_CONSTRAINTS)
|
||||
.setInputData(data)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildWorkIdentifier(roomId: String): String {
|
||||
return "${roomId}_$SEND_WORK"
|
||||
}
|
||||
}
|
|
@ -18,20 +18,13 @@ package im.vector.matrix.android.internal.session.room.timeline
|
|||
|
||||
import arrow.core.Try
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.helper.addAll
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvents
|
||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||
import im.vector.matrix.android.internal.database.helper.isUnlinked
|
||||
import im.vector.matrix.android.internal.database.helper.merge
|
||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
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.create
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||
import io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
|
@ -39,8 +32,7 @@ import timber.log.Timber
|
|||
/**
|
||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||
*/
|
||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
|
@ -119,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
|
@ -142,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
|
@ -151,8 +143,6 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
|
||||
//Event
|
||||
eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
|
||||
// Then we merge chunks if needed
|
||||
if (currentChunk != prevChunk && prevChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||
|
|
|
@ -31,14 +31,9 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
|
|||
import im.vector.matrix.android.internal.database.query.find
|
||||
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.EventRelationsAggregationUpdater
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.sync.model.InvitedRoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSync
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncAccountData
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncEphemeral
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomsSyncResponse
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
|
@ -46,8 +41,7 @@ import timber.log.Timber
|
|||
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||
private val roomTagHandler: RoomTagHandler,
|
||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
||||
private val roomTagHandler: RoomTagHandler) {
|
||||
|
||||
sealed class HandlingStrategy {
|
||||
data class JOINED(val data: Map<String, RoomSync>) : HandlingStrategy()
|
||||
|
@ -67,9 +61,9 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||
|
||||
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
|
||||
val rooms = when (handlingStrategy) {
|
||||
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
|
||||
is HandlingStrategy.JOINED -> handlingStrategy.data.map { handleJoinedRoom(realm, it.key, it.value) }
|
||||
is HandlingStrategy.INVITED -> handlingStrategy.data.map { handleInvitedRoom(realm, it.key, it.value) }
|
||||
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
|
||||
is HandlingStrategy.LEFT -> handlingStrategy.data.map { handleLeftRoom(realm, it.key, it.value) }
|
||||
}
|
||||
realm.insertOrUpdate(rooms)
|
||||
}
|
||||
|
@ -81,7 +75,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||
Timber.v("Handle join sync for room $roomId")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
if (roomEntity.membership == Membership.INVITE) {
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
|
@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||
transactionIds.forEach {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
}
|
||||
}
|
||||
}
|
||||
eventRelationsAggregationUpdater.update(realm, roomId, roomSync.timeline?.events)
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
|
@ -139,7 +135,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||
InvitedRoomSync): RoomEntity {
|
||||
Timber.v("Handle invited sync for room $roomId")
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
roomEntity.membership = Membership.INVITE
|
||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
|
||||
|
@ -153,7 +149,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||
roomId: String,
|
||||
roomSync: RoomSync): RoomEntity {
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
roomEntity.membership = Membership.LEAVE
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
|
|
|
@ -40,7 +40,7 @@ internal class SyncModule {
|
|||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
RoomSyncHandler(get(), get(), get(), get(), get())
|
||||
RoomSyncHandler(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
scope(DefaultSession.SCOPE) {
|
||||
|
|
|
@ -90,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||
avatarUrl = avatarUrl,
|
||||
memberName = formattedMemberName,
|
||||
showInformation = showInformation,
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map { Triple(it.key, it.count, it.addedByMe) },
|
||||
orderedReactionList = event.annotations?.reactionsSummary?.map {
|
||||
ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty())
|
||||
},
|
||||
hasBeenEdited = hasBeenEdited
|
||||
)
|
||||
|
||||
|
|
|
@ -112,11 +112,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||
reactionButton.isVisible = true
|
||||
reactionButton.reactedListener = reactionClickListener
|
||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
|
||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
|
||||
idToRefInFlow.add(reactionButton.id)
|
||||
reactionButton.reactionString = reaction.first
|
||||
reactionButton.reactionCount = reaction.second
|
||||
reactionButton.setChecked(reaction.third)
|
||||
reactionButton.reactionString = reaction.key
|
||||
reactionButton.reactionCount = reaction.count
|
||||
reactionButton.setChecked(reaction.addedByMe)
|
||||
reactionButton.isEnabled = reaction.synced
|
||||
}
|
||||
}
|
||||
// Just setting the view as gone will break the FlowHelper (and invisible will take too much space),
|
||||
|
|
|
@ -16,9 +16,8 @@
|
|||
|
||||
package im.vector.riotredesign.features.home.room.detail.timeline.item
|
||||
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
|
||||
import android.os.Parcelable
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
|
@ -31,6 +30,15 @@ data class MessageInformationData(
|
|||
val memberName: CharSequence? = null,
|
||||
val showInformation: Boolean = true,
|
||||
/*List of reactions (emoji,count,isSelected)*/
|
||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null,
|
||||
var orderedReactionList: List<ReactionInfoData>? = null,
|
||||
var hasBeenEdited: Boolean = false
|
||||
) : Parcelable
|
||||
) : Parcelable
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class ReactionInfoData(
|
||||
val key: String,
|
||||
val count: Int,
|
||||
val addedByMe: Boolean,
|
||||
val synced: Boolean
|
||||
) : Parcelable
|
||||
|
|
Loading…
Reference in a new issue