forked from GitHub-Mirror/riotX-android
Local echo for reactions/edits/redacts
This commit is contained in:
parent
466be1dca5
commit
99925d7cf9
@ -48,7 +48,7 @@ android {
|
|||||||
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
buildConfigField "boolean", "LOG_PRIVATE_DATA", "false"
|
||||||
|
|
||||||
// Set to BODY instead of NONE to enable logging
|
// 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 {
|
release {
|
||||||
|
@ -18,12 +18,10 @@ package im.vector.matrix.android.session.room.timeline
|
|||||||
|
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.InstrumentedTest
|
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.Timeline
|
||||||
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.database.model.SessionRealmModule
|
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.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.membership.SenderRoomMemberExtractor
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultTimeline
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
import im.vector.matrix.android.internal.session.room.timeline.TimelineEventFactory
|
||||||
@ -60,8 +58,7 @@ internal class TimelineTest : InstrumentedTest {
|
|||||||
|
|
||||||
private fun createTimeline(initialEventId: String? = null): Timeline {
|
private fun createTimeline(initialEventId: String? = null): Timeline {
|
||||||
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
val taskExecutor = TaskExecutor(testCoroutineDispatchers)
|
||||||
val erau = EventRelationsAggregationUpdater(Credentials("", "", "", null, null))
|
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy)
|
||||||
val tokenChunkEventPersistor = TokenChunkEventPersistor(monarchy, erau)
|
|
||||||
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
val paginationTask = FakePaginationTask(tokenChunkEventPersistor)
|
||||||
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
val getContextOfEventTask = FakeGetContextOfEventTask(tokenChunkEventPersistor)
|
||||||
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
val roomMemberExtractor = SenderRoomMemberExtractor(ROOM_ID)
|
||||||
|
@ -21,5 +21,6 @@ data class EditAggregatedSummary(
|
|||||||
val aggregatedContent: Content? = null,
|
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)
|
// 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 sourceEvents: List<String>,
|
||||||
|
val localEchos: List<String>,
|
||||||
val lastEditTs: Long = 0
|
val lastEditTs: Long = 0
|
||||||
)
|
)
|
||||||
|
@ -5,5 +5,6 @@ data class ReactionAggregatedSummary(
|
|||||||
val count: Int, // 8
|
val count: Int, // 8
|
||||||
val addedByMe: Boolean, // true
|
val addedByMe: Boolean, // true
|
||||||
val firstTimestamp: Long, // unix timestamp
|
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.count,
|
||||||
it.addedByMe,
|
it.addedByMe,
|
||||||
it.firstTimestamp,
|
it.firstTimestamp,
|
||||||
it.sourceEvents.toList()
|
it.sourceEvents.toList(),
|
||||||
|
it.sourceLocalEcho.toList()
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
editSummary = annotationsSummary.editSummary?.let {
|
editSummary = annotationsSummary.editSummary?.let {
|
||||||
EditAggregatedSummary(
|
EditAggregatedSummary(
|
||||||
ContentMapper.map(it.aggregatedContent),
|
ContentMapper.map(it.aggregatedContent),
|
||||||
it.sourceEvents.toList(),
|
it.sourceEvents.toList(),
|
||||||
|
it.sourceLocalEchoEvents.toList(),
|
||||||
it.lastEditTs
|
it.lastEditTs
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ internal open class EditAggregatedSummaryEntity(
|
|||||||
var aggregatedContent: String? = null,
|
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)
|
// 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(),
|
||||||
|
var sourceLocalEchoEvents: RealmList<String> = RealmList(),
|
||||||
var lastEditTs: Long = 0
|
var lastEditTs: Long = 0
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ internal open class ReactionAggregatedSummaryEntity(
|
|||||||
// The first time this reaction was added (for ordering purpose)
|
// The first time this reaction was added (for ordering purpose)
|
||||||
var firstTimestamp: Long = 0,
|
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)
|
// 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() {
|
) : RealmObject() {
|
||||||
|
|
||||||
companion object
|
companion object
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.matrix.android.internal.database.query
|
package im.vector.matrix.android.internal.database.query
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
|
import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.*
|
||||||
@ -42,12 +43,36 @@ internal fun EventEntity.Companion.where(realm: Realm,
|
|||||||
query.equalTo(EventEntityFields.TYPE, type)
|
query.equalTo(EventEntityFields.TYPE, type)
|
||||||
}
|
}
|
||||||
return when (linkFilterMode) {
|
return when (linkFilterMode) {
|
||||||
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
|
LINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, false)
|
||||||
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
|
UNLINKED_ONLY -> query.equalTo(EventEntityFields.IS_UNLINKED, true)
|
||||||
BOTH -> query
|
BOTH -> query
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//internal fun EventEntity.Companion.unsent(realm: Realm,
|
||||||
|
// roomId: String? = null): RealmQuery<EventEntity> {
|
||||||
|
// val query = realm.where<EventEntity>()
|
||||||
|
// if (roomId != null) {
|
||||||
|
// query.equalTo(EventEntityFields.ROOM_ID, roomId)
|
||||||
|
// }
|
||||||
|
// query.equalTo(EventEntityFields.SEND_STATE_STR, SendState.UNSENT.name)
|
||||||
|
// return query
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//internal fun EventEntity.Companion.byTypes(realm: Realm,
|
||||||
|
// types: List<String>): RealmQuery<EventEntity> {
|
||||||
|
// val query = realm.where<EventEntity>()
|
||||||
|
// types.forEachIndexed { index, type ->
|
||||||
|
// if (index == 0) {
|
||||||
|
// query.equalTo(EventEntityFields.TYPE, type)
|
||||||
|
// } else {
|
||||||
|
// query.or().equalTo(EventEntityFields.TYPE, type)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return query
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
internal fun EventEntity.Companion.latestEvent(realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
includedTypes: List<String> = emptyList(),
|
includedTypes: List<String> = emptyList(),
|
||||||
|
@ -105,10 +105,6 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||||||
RoomSummaryUpdater(get(), get(), get())
|
RoomSummaryUpdater(get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
|
||||||
EventRelationsAggregationUpdater(get())
|
|
||||||
}
|
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
DefaultRoomService(get(), get(), get(), get()) as RoomService
|
||||||
}
|
}
|
||||||
@ -168,9 +164,11 @@ internal class SessionModule(private val sessionParams: SessionParams) {
|
|||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
val groupSummaryUpdater = GroupSummaryUpdater(get())
|
||||||
val eventsPruner = EventsPruner(get(), get(), get(), get())
|
|
||||||
val userEntityUpdater = UserEntityUpdater(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -15,16 +15,14 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.session.room
|
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.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.session.events.model.*
|
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionContent
|
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
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.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
|
import timber.log.Timber
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -32,198 +30,34 @@ import timber.log.Timber
|
|||||||
* For reactions will build a EventAnnotationsSummaryEntity, ans for edits a EditAggregatedSummaryEntity.
|
* 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.
|
* 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>?) {
|
override val query = Monarchy.Query<EventEntity> {
|
||||||
events?.forEach { event ->
|
EventEntity.where(it)
|
||||||
when (event.type) {
|
//mmm why is this query not working?
|
||||||
EventType.REACTION -> {
|
// EventEntity.byTypes(it, listOf(
|
||||||
//we got a reaction!!
|
// EventType.REDACTION, EventType.MESSAGE, EventType.REDACTION)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleReplace(event: Event, content: MessageContent, roomId: String, realm: Realm) {
|
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||||
val eventId = event.eventId ?: return
|
Timber.v("EventRelationsAggregationUpdater called with ${inserted.size} insertions")
|
||||||
val targetEventId = content.relatesTo?.eventId ?: return
|
val inserted = inserted
|
||||||
val newContent = content.newContent ?: return
|
.mapNotNull { it.asDomain() to it.sendState }
|
||||||
//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 params = EventRelationsAggregationTask.Params(
|
||||||
val existingSummary = existing.editSummary
|
inserted,
|
||||||
if (existingSummary == null) {
|
credentials.userId
|
||||||
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)
|
|
||||||
|
|
||||||
existing.editSummary = editSummary
|
task.configureWith(params)
|
||||||
} else {
|
.executeBy(taskExecutor)
|
||||||
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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
package im.vector.matrix.android.internal.session.room
|
||||||
|
|
||||||
import im.vector.matrix.android.internal.session.DefaultSession
|
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.CreateRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
|
||||||
import im.vector.matrix.android.internal.session.room.membership.DefaultLoadRoomMembersTask
|
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.prune.PruneEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask
|
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.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.send.LocalEchoEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
import im.vector.matrix.android.internal.session.room.state.DefaultSendStateTask
|
||||||
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
import im.vector.matrix.android.internal.session.room.state.SendStateTask
|
||||||
@ -57,7 +57,7 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
TokenChunkEventPersistor(get(), get())
|
TokenChunkEventPersistor(get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
@ -109,7 +109,11 @@ class RoomModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
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.database.query.where
|
||||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
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,
|
internal class EventsPruner(monarchy: Monarchy,
|
||||||
private val credentials: Credentials,
|
private val credentials: Credentials,
|
||||||
private val pruneEventTask: PruneEventTask,
|
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 val query = Monarchy.Query<EventEntity> { EventEntity.where(it, type = EventType.REDACTION) }
|
||||||
|
|
||||||
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
override fun processChanges(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
|
||||||
|
Timber.v("Event pruner called with ${inserted.size} insertions")
|
||||||
val redactionEvents = inserted
|
val redactionEvents = inserted
|
||||||
.mapNotNull { it.asDomain() }
|
.mapNotNull { it.asDomain() }
|
||||||
|
|
||||||
|
@ -17,14 +17,15 @@ package im.vector.matrix.android.internal.session.room.prune
|
|||||||
|
|
||||||
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.events.model.*
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
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.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.EventEntity
|
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
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.task.Task
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionSync
|
import im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
@ -41,8 +42,7 @@ internal interface PruneEventTask : Task<PruneEventTask.Params, Unit> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
internal class DefaultPruneEventTask(
|
internal class DefaultPruneEventTask(
|
||||||
private val monarchy: Monarchy,
|
private val monarchy: Monarchy) : PruneEventTask {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) : PruneEventTask {
|
|
||||||
|
|
||||||
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
override fun execute(params: PruneEventTask.Params): Try<Unit> {
|
||||||
return monarchy.tryTransactionSync { realm ->
|
return monarchy.tryTransactionSync { realm ->
|
||||||
@ -57,6 +57,12 @@ internal class DefaultPruneEventTask(
|
|||||||
return
|
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()
|
val eventToPrune = EventEntity.where(realm, eventId = redactionEvent.redacts).findFirst()
|
||||||
?: return
|
?: return
|
||||||
|
|
||||||
@ -72,19 +78,19 @@ internal class DefaultPruneEventTask(
|
|||||||
?: 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>()
|
||||||
if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
// if (RelationType.REPLACE == contentModel?.relatesTo?.type && contentModel.relatesTo?.eventId != null) {
|
||||||
eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
// eventRelationsAggregationUpdater.handleRedactionOfReplace(eventToPrune, contentModel.relatesTo!!.eventId!!, realm)
|
||||||
}
|
// }
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
}
|
}
|
||||||
EventType.REACTION -> {
|
// EventType.REACTION -> {
|
||||||
eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
// eventRelationsAggregationUpdater.handleReactionRedact(eventToPrune, realm, userId)
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,12 +15,12 @@
|
|||||||
*/
|
*/
|
||||||
package im.vector.matrix.android.internal.session.room.relation
|
package im.vector.matrix.android.internal.session.room.relation
|
||||||
|
|
||||||
import androidx.work.*
|
import androidx.work.OneTimeWorkRequest
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
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.room.model.relation.RelationService
|
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageType
|
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.api.util.Cancelable
|
||||||
import im.vector.matrix.android.internal.database.helper.addSendingEvent
|
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.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.LocalEchoEventFactory
|
||||||
import im.vector.matrix.android.internal.session.room.send.RedactEventWorker
|
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.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.TaskExecutor
|
||||||
import im.vector.matrix.android.internal.task.configureWith
|
import im.vector.matrix.android.internal.task.configureWith
|
||||||
import im.vector.matrix.android.internal.util.CancelableWork
|
import im.vector.matrix.android.internal.util.CancelableWork
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
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,
|
internal class DefaultRelationService(private val roomId: String,
|
||||||
private val eventFactory: LocalEchoEventFactory,
|
private val eventFactory: LocalEchoEventFactory,
|
||||||
@ -55,28 +50,22 @@ internal class DefaultRelationService(private val roomId: String,
|
|||||||
|
|
||||||
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
|
override fun sendReaction(reaction: String, targetEventId: String): Cancelable {
|
||||||
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
val event = eventFactory.createReactionEvent(roomId, targetEventId, reaction)
|
||||||
// .also {
|
.also {
|
||||||
// //saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
// }
|
}
|
||||||
val sendRelationWork = createSendRelationWork(event)
|
val sendRelationWork = createSendRelationWork(event)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, sendRelationWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, sendRelationWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(sendRelationWork.id)
|
return CancelableWork(sendRelationWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
private fun createSendRelationWork(event: Event): OneTimeWorkRequest {
|
||||||
//TODO use the new API to send relation (for now use regular send)
|
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(
|
val sendContentWorkerParams = SendEventWorker.Params(
|
||||||
roomId, event)
|
roomId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(sendWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
override fun undoReaction(reaction: String, targetEventId: String, myUserId: String)/*: Cancelable*/ {
|
||||||
@ -91,11 +80,19 @@ internal class DefaultRelationService(private val roomId: String,
|
|||||||
.enableRetry()
|
.enableRetry()
|
||||||
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
.dispatchTo(object : MatrixCallback<FindReactionEventForUndoTask.Result> {
|
||||||
override fun onSuccess(data: 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 ->
|
data.redactEventId?.let { toRedact ->
|
||||||
val redactWork = createRedactEventWork(toRedact, null)
|
|
||||||
WorkManager.getInstance()
|
val redactEvent = eventFactory.createRedactEvent(roomId, toRedact, null).also {
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
saveLocalEcho(it)
|
||||||
.enqueue()
|
}
|
||||||
|
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) {
|
override fun onSuccess(data: UpdateQuickReactionTask.Result) {
|
||||||
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
|
data.reactionToAdd?.also { sendReaction(it, targetEventId) }
|
||||||
data.reactionToRedact.forEach {
|
data.reactionToRedact.forEach {
|
||||||
val redactWork = createRedactEventWork(it, null)
|
val redactEvent = eventFactory.createRedactEvent(roomId, it, null).also {
|
||||||
WorkManager.getInstance()
|
saveLocalEcho(it)
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
}
|
||||||
.enqueue()
|
val redactWork = createRedactEventWork(redactEvent, it, null)
|
||||||
|
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -133,48 +131,26 @@ internal class DefaultRelationService(private val roomId: String,
|
|||||||
return "${roomId}_$identifier"
|
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?
|
//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(localEvent.eventId!!,
|
||||||
|
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(
|
|
||||||
roomId, eventId, reason)
|
roomId, eventId, reason)
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(redactWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun editTextMessage(targetEventId: String, newBodyText: String, newBodyAutoMarkdown: Boolean, compatibilityBodyText: String): Cancelable {
|
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 sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
//TODO use relation API?
|
//TODO use relation API?
|
||||||
val workRequest = OneTimeWorkRequestBuilder<SendEventWorker>()
|
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(sendWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
WorkManager.getInstance()
|
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
.beginUniqueWork(buildWorkIdentifier(REACTION_WORK), ExistingWorkPolicy.APPEND, workRequest)
|
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(workRequest.id)
|
return CancelableWork(workRequest.id)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -186,18 +162,19 @@ internal class DefaultRelationService(private val roomId: String,
|
|||||||
} ?: return null
|
} ?: return null
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
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)
|
val workRequest = TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
.enqueue()
|
TimelineSendEventWorkCommon.postWork(roomId, workRequest)
|
||||||
return CancelableWork(workRequest.id)
|
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) {
|
private fun saveLocalEcho(event: Event) {
|
||||||
monarchy.tryTransactionAsync { realm ->
|
monarchy.tryTransactionAsync { realm ->
|
||||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst()
|
||||||
|
@ -70,7 +70,11 @@ class SendRelationWorker(context: Context, params: WorkerParameters)
|
|||||||
return result.fold({
|
return result.fold({
|
||||||
when (it) {
|
when (it) {
|
||||||
is Failure.NetworkConnection -> Result.retry()
|
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() })
|
}, { 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.Cancelable
|
||||||
import im.vector.matrix.android.api.util.CancelableBag
|
import im.vector.matrix.android.api.util.CancelableBag
|
||||||
import im.vector.matrix.android.api.util.addTo
|
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.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.CancelableWork
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private const val SEND_WORK = "SEND_WORK"
|
|
||||||
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
private const val UPLOAD_WORK = "UPLOAD_WORK"
|
||||||
private const val BACKOFF_DELAY = 10_000L
|
private const val BACKOFF_DELAY = 10_000L
|
||||||
|
|
||||||
@ -54,9 +48,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(event)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(sendWork.id)
|
return CancelableWork(sendWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,9 +57,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
saveLocalEcho(it)
|
saveLocalEcho(it)
|
||||||
}
|
}
|
||||||
val sendWork = createSendEventWork(event)
|
val sendWork = createSendEventWork(event)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, sendWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, sendWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(sendWork.id)
|
return CancelableWork(sendWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,12 +70,9 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
||||||
//TODO manage local echo ?
|
|
||||||
//TODO manage media/attachements?
|
//TODO manage media/attachements?
|
||||||
val redactWork = createRedactEventWork(event, reason)
|
val redactWork = createRedactEventWork(event, reason)
|
||||||
WorkManager.getInstance()
|
TimelineSendEventWorkCommon.postWork(roomId, redactWork)
|
||||||
.beginUniqueWork(buildWorkIdentifier(SEND_WORK), ExistingWorkPolicy.APPEND, redactWork)
|
|
||||||
.enqueue()
|
|
||||||
return CancelableWork(redactWork.id)
|
return CancelableWork(redactWork.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,14 +93,7 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun saveLocalEcho(event: Event) {
|
private fun saveLocalEcho(event: Event) {
|
||||||
monarchy.tryTransactionAsync { realm ->
|
eventFactory.saveLocalEcho(monarchy, event)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildWorkIdentifier(identifier: String): String {
|
private fun buildWorkIdentifier(identifier: String): String {
|
||||||
@ -124,26 +104,20 @@ internal class DefaultSendService(private val roomId: String,
|
|||||||
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
val sendContentWorkerParams = SendEventWorker.Params(roomId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<SendEventWorker>()
|
return TimelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData)
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(sendWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
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(
|
val sendContentWorkerParams = RedactEventWorker.Params(redactEvent.eventId!!,
|
||||||
roomId, event.eventId!!, reason)
|
roomId, event.eventId, reason)
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
||||||
|
|
||||||
return OneTimeWorkRequestBuilder<RedactEventWorker>()
|
return TimelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData)
|
||||||
.setConstraints(WORK_CONSTRAINTS)
|
|
||||||
.setInputData(redactWorkData)
|
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
|
||||||
.build()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
private fun createUploadMediaWork(event: Event, attachment: ContentAttachmentData): OneTimeWorkRequest {
|
||||||
|
@ -18,21 +18,37 @@ package im.vector.matrix.android.internal.session.room.send
|
|||||||
|
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.R
|
import im.vector.matrix.android.R
|
||||||
import im.vector.matrix.android.api.auth.data.Credentials
|
import im.vector.matrix.android.api.auth.data.Credentials
|
||||||
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
import im.vector.matrix.android.api.permalinks.PermalinkFactory
|
||||||
import im.vector.matrix.android.api.session.content.ContentAttachmentData
|
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.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.ReactionContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReactionInfo
|
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.RelationDefaultContent
|
||||||
import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
|
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.session.content.ThumbnailExtractor
|
||||||
import im.vector.matrix.android.internal.util.StringProvider
|
import im.vector.matrix.android.internal.util.StringProvider
|
||||||
|
import im.vector.matrix.android.internal.util.tryTransactionAsync
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.commonmark.renderer.html.HtmlRenderer
|
import org.commonmark.renderer.html.HtmlRenderer
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) {
|
internal class LocalEchoEventFactory(private val credentials: Credentials, private val stringProvider: StringProvider) {
|
||||||
|
|
||||||
fun createTextEvent(roomId: String, msgType: String, text: String, autoMarkdown: Boolean): Event {
|
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 document = parser.parse(text)
|
||||||
val renderer = HtmlRenderer.builder().build()
|
val renderer = HtmlRenderer.builder().build()
|
||||||
val htmlText = renderer.render(document)
|
val htmlText = renderer.render(document)
|
||||||
if (!TextUtils.equals(text, htmlText)) {
|
if (isFormattedTextPertinent(text, htmlText)) { //FIX that
|
||||||
return createFormattedTextEvent(roomId, text, htmlText)
|
return createFormattedTextEvent(roomId, text, htmlText)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -49,6 +65,9 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||||||
return createEvent(roomId, content)
|
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 {
|
fun createFormattedTextEvent(roomId: String, text: String, formattedText: String): Event {
|
||||||
val content = MessageTextContent(
|
val content = MessageTextContent(
|
||||||
type = MessageType.MSGTYPE_TEXT,
|
type = MessageType.MSGTYPE_TEXT,
|
||||||
@ -71,7 +90,7 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||||||
val document = parser.parse(newBodyText)
|
val document = parser.parse(newBodyText)
|
||||||
val renderer = HtmlRenderer.builder().build()
|
val renderer = HtmlRenderer.builder().build()
|
||||||
val htmlText = renderer.render(document)
|
val htmlText = renderer.render(document)
|
||||||
if (!TextUtils.equals(newBodyText, htmlText)) {
|
if (isFormattedTextPertinent(newBodyText, htmlText)) {
|
||||||
newContent = MessageTextContent(
|
newContent = MessageTextContent(
|
||||||
type = MessageType.MSGTYPE_TEXT,
|
type = MessageType.MSGTYPE_TEXT,
|
||||||
format = MessageType.FORMAT_MATRIX_HTML,
|
format = MessageType.FORMAT_MATRIX_HTML,
|
||||||
@ -107,14 +126,16 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||||||
reaction
|
reaction
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
val localId = dummyEventId(roomId)
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
originServerTs = dummyOriginServerTs(),
|
originServerTs = dummyOriginServerTs(),
|
||||||
sender = credentials.userId,
|
sender = credentials.userId,
|
||||||
eventId = dummyEventId(roomId),
|
eventId = localId,
|
||||||
type = EventType.REACTION,
|
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 {
|
private fun createEvent(roomId: String, content: Any? = null): Event {
|
||||||
|
val localID = dummyEventId(roomId)
|
||||||
return Event(
|
return Event(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
originServerTs = dummyOriginServerTs(),
|
originServerTs = dummyOriginServerTs(),
|
||||||
sender = credentials.userId,
|
sender = credentials.userId,
|
||||||
eventId = dummyEventId(roomId),
|
eventId = localID,
|
||||||
type = EventType.MESSAGE,
|
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 {
|
private fun dummyEventId(roomId: String): String {
|
||||||
return roomId + "-" + dummyOriginServerTs()
|
return "m.${txNCounter++}"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
|
fun createReplyTextEvent(roomId: String, eventReplied: Event, replyText: String): Event? {
|
||||||
@ -285,4 +308,49 @@ internal class LocalEchoEventFactory(private val credentials: Credentials, priva
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* {
|
||||||
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var txNCounter = System.currentTimeMillis()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.session.room.RoomAPI
|
||||||
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
import im.vector.matrix.android.internal.util.WorkerParamsFactory
|
||||||
import org.koin.standalone.inject
|
import org.koin.standalone.inject
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
||||||
: Worker(context, params), MatrixKoinComponent {
|
: Worker(context, params), MatrixKoinComponent {
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
internal data class Params(
|
internal data class Params(
|
||||||
|
val txID: String,
|
||||||
val roomId: String,
|
val roomId: String,
|
||||||
val eventId: String,
|
val eventId: String,
|
||||||
val reason: String?
|
val reason: String?
|
||||||
@ -40,26 +40,26 @@ internal class RedactEventWorker(context: Context, params: WorkerParameters)
|
|||||||
private val roomAPI by inject<RoomAPI>()
|
private val roomAPI by inject<RoomAPI>()
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val params = WorkerParamsFactory.fromData<RedactEventWorker.Params>(inputData)
|
val params = WorkerParamsFactory.fromData<Params>(inputData)
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
|
|
||||||
if (params.eventId == null) {
|
val eventId = params.eventId
|
||||||
return Result.failure()
|
|
||||||
}
|
|
||||||
val txID = UUID.randomUUID().toString()
|
|
||||||
|
|
||||||
val result = executeRequest<SendResponse> {
|
val result = executeRequest<SendResponse> {
|
||||||
apiCall = roomAPI.redactEvent(
|
apiCall = roomAPI.redactEvent(
|
||||||
txID,
|
params.txID,
|
||||||
params.roomId,
|
params.roomId,
|
||||||
params.eventId,
|
eventId,
|
||||||
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
if (params.reason == null) emptyMap() else mapOf("reason" to params.reason)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return result.fold({
|
return result.fold({
|
||||||
when (it) {
|
when (it) {
|
||||||
is Failure.NetworkConnection -> Result.retry()
|
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()
|
Result.success()
|
||||||
|
@ -61,7 +61,11 @@ internal class SendEventWorker(context: Context, params: WorkerParameters)
|
|||||||
return result.fold({
|
return result.fold({
|
||||||
when (it) {
|
when (it) {
|
||||||
is Failure.NetworkConnection -> Result.retry()
|
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() })
|
}, { 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 arrow.core.Try
|
||||||
import com.zhuinden.monarchy.Monarchy
|
import com.zhuinden.monarchy.Monarchy
|
||||||
import im.vector.matrix.android.internal.database.helper.addAll
|
import im.vector.matrix.android.internal.database.helper.*
|
||||||
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.model.ChunkEntity
|
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.model.RoomEntity
|
||||||
import im.vector.matrix.android.internal.database.query.create
|
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.find
|
||||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
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 im.vector.matrix.android.internal.util.tryTransactionSync
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -39,8 +32,7 @@ import timber.log.Timber
|
|||||||
/**
|
/**
|
||||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||||
*/
|
*/
|
||||||
internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
internal class TokenChunkEventPersistor(private val monarchy: Monarchy) {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <pre>
|
* <pre>
|
||||||
@ -119,7 +111,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
val nextToken: String?
|
val nextToken: String?
|
||||||
val prevToken: String?
|
val prevToken: String?
|
||||||
@ -142,7 +134,7 @@ internal class TokenChunkEventPersistor(private val monarchy: Monarchy,
|
|||||||
} else {
|
} else {
|
||||||
nextChunk?.apply { this.prevToken = prevToken }
|
nextChunk?.apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||||
|
|
||||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||||
Timber.v("Reach end of $roomId")
|
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}")
|
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||||
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
currentChunk.addAll(roomId, receivedChunk.events, direction, isUnlinked = currentChunk.isUnlinked())
|
||||||
|
|
||||||
//Event
|
|
||||||
eventRelationsAggregationUpdater.update(realm,roomId,receivedChunk.events.toList())
|
|
||||||
// Then we merge chunks if needed
|
// Then we merge chunks if needed
|
||||||
if (currentChunk != prevChunk && prevChunk != null) {
|
if (currentChunk != prevChunk && prevChunk != null) {
|
||||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
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.find
|
||||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
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.RoomSummaryUpdater
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
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.*
|
||||||
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 io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.kotlin.createObject
|
import io.realm.kotlin.createObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@ -46,8 +41,7 @@ import timber.log.Timber
|
|||||||
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
internal class RoomSyncHandler(private val monarchy: Monarchy,
|
||||||
private val readReceiptHandler: ReadReceiptHandler,
|
private val readReceiptHandler: ReadReceiptHandler,
|
||||||
private val roomSummaryUpdater: RoomSummaryUpdater,
|
private val roomSummaryUpdater: RoomSummaryUpdater,
|
||||||
private val roomTagHandler: RoomTagHandler,
|
private val roomTagHandler: RoomTagHandler) {
|
||||||
private val eventRelationsAggregationUpdater: EventRelationsAggregationUpdater) {
|
|
||||||
|
|
||||||
sealed class HandlingStrategy {
|
sealed class HandlingStrategy {
|
||||||
data class JOINED(val data: Map<String, RoomSync>) : 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) {
|
private fun handleRoomSync(realm: Realm, handlingStrategy: HandlingStrategy) {
|
||||||
val rooms = when (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.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)
|
realm.insertOrUpdate(rooms)
|
||||||
}
|
}
|
||||||
@ -81,7 +75,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
Timber.v("Handle join sync for room $roomId")
|
Timber.v("Handle join sync for room $roomId")
|
||||||
|
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
if (roomEntity.membership == Membership.INVITE) {
|
if (roomEntity.membership == Membership.INVITE) {
|
||||||
roomEntity.chunks.deleteAllFromRealm()
|
roomEntity.chunks.deleteAllFromRealm()
|
||||||
@ -116,11 +110,13 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
transactionIds.forEach {
|
transactionIds.forEach {
|
||||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||||
if (sendingEventEntity != null) {
|
if (sendingEventEntity != null) {
|
||||||
|
Timber.v("Remove local echo for tx:$it")
|
||||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
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)
|
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications)
|
||||||
|
|
||||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||||
@ -139,7 +135,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
InvitedRoomSync): RoomEntity {
|
InvitedRoomSync): RoomEntity {
|
||||||
Timber.v("Handle invited sync for room $roomId")
|
Timber.v("Handle invited sync for room $roomId")
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
roomEntity.membership = Membership.INVITE
|
roomEntity.membership = Membership.INVITE
|
||||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||||
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
|
val chunkEntity = handleTimelineEvents(realm, roomId, roomSync.inviteState.events)
|
||||||
@ -153,7 +149,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
|
|||||||
roomId: String,
|
roomId: String,
|
||||||
roomSync: RoomSync): RoomEntity {
|
roomSync: RoomSync): RoomEntity {
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||||
?: realm.createObject(roomId)
|
?: realm.createObject(roomId)
|
||||||
|
|
||||||
roomEntity.membership = Membership.LEAVE
|
roomEntity.membership = Membership.LEAVE
|
||||||
roomEntity.chunks.deleteAllFromRealm()
|
roomEntity.chunks.deleteAllFromRealm()
|
||||||
|
@ -40,7 +40,7 @@ internal class SyncModule {
|
|||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
RoomSyncHandler(get(), get(), get(), get(), get())
|
RoomSyncHandler(get(), get(), get(), get())
|
||||||
}
|
}
|
||||||
|
|
||||||
scope(DefaultSession.SCOPE) {
|
scope(DefaultSession.SCOPE) {
|
||||||
|
@ -90,7 +90,9 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
|
|||||||
avatarUrl = avatarUrl,
|
avatarUrl = avatarUrl,
|
||||||
memberName = formattedMemberName,
|
memberName = formattedMemberName,
|
||||||
showInformation = showInformation,
|
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
|
hasBeenEdited = hasBeenEdited
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -112,11 +112,12 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
|
|||||||
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
(holder.reactionWrapper?.children?.elementAtOrNull(index) as? ReactionButton)?.let { reactionButton ->
|
||||||
reactionButton.isVisible = true
|
reactionButton.isVisible = true
|
||||||
reactionButton.reactedListener = reactionClickListener
|
reactionButton.reactedListener = reactionClickListener
|
||||||
reactionButton.setTag(R.id.messageBottomInfo, reaction.first)
|
reactionButton.setTag(R.id.messageBottomInfo, reaction.key)
|
||||||
idToRefInFlow.add(reactionButton.id)
|
idToRefInFlow.add(reactionButton.id)
|
||||||
reactionButton.reactionString = reaction.first
|
reactionButton.reactionString = reaction.key
|
||||||
reactionButton.reactionCount = reaction.second
|
reactionButton.reactionCount = reaction.count
|
||||||
reactionButton.setChecked(reaction.third)
|
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),
|
// 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
|
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 android.os.Parcelable
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@ -31,6 +30,15 @@ data class MessageInformationData(
|
|||||||
val memberName: CharSequence? = null,
|
val memberName: CharSequence? = null,
|
||||||
val showInformation: Boolean = true,
|
val showInformation: Boolean = true,
|
||||||
/*List of reactions (emoji,count,isSelected)*/
|
/*List of reactions (emoji,count,isSelected)*/
|
||||||
var orderedReactionList: List<Triple<String,Int,Boolean>>? = null,
|
var orderedReactionList: List<ReactionInfoData>? = null,
|
||||||
var hasBeenEdited: Boolean = false
|
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 New Issue
Block a user