Compare commits
14 Commits
develop
...
feature/sy
Author | SHA1 | Date | |
---|---|---|---|
|
be2e5117ca | ||
|
35bd3946b5 | ||
|
26c8b90e80 | ||
|
2348ceca60 | ||
|
4def1e0d76 | ||
|
1aa65dad7f | ||
|
1c4cef9115 | ||
|
20559b9fc4 | ||
|
990a266d83 | ||
|
56e15cf054 | ||
|
f8db684ed2 | ||
|
3d2cf49f53 | ||
|
f07ee343fd | ||
|
fb4b42db32 |
@ -28,7 +28,6 @@ import im.vector.matrix.android.session.room.timeline.RoomDataHelper.createFakeR
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmConfiguration
|
||||
import io.realm.kotlin.createObject
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.amshove.kluent.shouldEqual
|
||||
import org.junit.Before
|
||||
@ -52,7 +51,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add(realm, "roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.timelineEvents.size shouldEqual 1
|
||||
}
|
||||
}
|
||||
@ -62,8 +61,8 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add(realm, "roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add(realm, "roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.timelineEvents.size shouldEqual 1
|
||||
}
|
||||
}
|
||||
@ -73,7 +72,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeRoomMemberEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add(realm, "roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 1
|
||||
}
|
||||
}
|
||||
@ -83,7 +82,7 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvent = createFakeMessageEvent()
|
||||
chunk.add("roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.add(realm, "roomId", fakeEvent, PaginationDirection.FORWARDS)
|
||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual 0
|
||||
}
|
||||
}
|
||||
@ -94,7 +93,9 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val chunk: ChunkEntity = realm.createObject()
|
||||
val fakeEvents = createFakeListOfEvents(30)
|
||||
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
|
||||
chunk.addAll("roomId", fakeEvents, PaginationDirection.FORWARDS)
|
||||
fakeEvents.forEach {
|
||||
chunk.add(realm, "roomId", it, PaginationDirection.FORWARDS)
|
||||
}
|
||||
chunk.lastStateIndex(PaginationDirection.FORWARDS) shouldEqual numberOfStateEvents
|
||||
}
|
||||
}
|
||||
@ -107,7 +108,9 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val numberOfStateEvents = fakeEvents.filter { it.isStateEvent() }.size
|
||||
val lastIsState = fakeEvents.last().isStateEvent()
|
||||
val expectedStateIndex = if (lastIsState) -numberOfStateEvents + 1 else -numberOfStateEvents
|
||||
chunk.addAll("roomId", fakeEvents, PaginationDirection.BACKWARDS)
|
||||
fakeEvents.forEach {
|
||||
chunk.add(realm, "roomId", it, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
chunk.lastStateIndex(PaginationDirection.BACKWARDS) shouldEqual expectedStateIndex
|
||||
}
|
||||
}
|
||||
@ -117,8 +120,12 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
val chunk1: ChunkEntity = realm.createObject()
|
||||
val chunk2: ChunkEntity = realm.createObject()
|
||||
chunk1.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||
chunk2.addAll("roomId", createFakeListOfEvents(30), PaginationDirection.BACKWARDS)
|
||||
createFakeListOfEvents(30).forEach {
|
||||
chunk1.add(realm, "roomId", it, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
createFakeListOfEvents(30).forEach {
|
||||
chunk2.add(realm, "roomId", it, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
||||
chunk1.timelineEvents.size shouldEqual 60
|
||||
}
|
||||
@ -133,14 +140,20 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
val eventsForChunk2 = eventsForChunk1 + createFakeListOfEvents(10)
|
||||
chunk1.isLastForward = true
|
||||
chunk2.isLastForward = false
|
||||
chunk1.addAll("roomId", eventsForChunk1, PaginationDirection.FORWARDS)
|
||||
chunk2.addAll("roomId", eventsForChunk2, PaginationDirection.BACKWARDS)
|
||||
eventsForChunk1.forEach {
|
||||
chunk1.add(realm, "roomId", it, PaginationDirection.FORWARDS)
|
||||
}
|
||||
eventsForChunk2.forEach {
|
||||
chunk2.add(realm, "roomId", it, PaginationDirection.BACKWARDS)
|
||||
}
|
||||
chunk1.merge("roomId", chunk2, PaginationDirection.BACKWARDS)
|
||||
chunk1.timelineEvents.size shouldEqual 40
|
||||
chunk1.isLastForward.shouldBeTrue()
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@Test
|
||||
fun merge_shouldEventsBeLinked_whenMergingLinkedWithUnlinked() {
|
||||
monarchy.runTransactionSync { realm ->
|
||||
@ -192,4 +205,6 @@ internal class ChunkEntityTest : InstrumentedTest {
|
||||
chunk1.nextToken shouldEqual nextToken
|
||||
}
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ internal class TimelineTest : InstrumentedTest {
|
||||
// val latch = CountDownLatch(2)
|
||||
// var timelineEvents: List<TimelineEvent> = emptyList()
|
||||
// timeline.listener = object : Timeline.Listener {
|
||||
// override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
// override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
// if (snapshot.isNotEmpty()) {
|
||||
// if (initialLoad == 0) {
|
||||
// initialLoad = snapshot.size
|
||||
|
@ -65,7 +65,7 @@ interface Timeline {
|
||||
|
||||
/**
|
||||
* This is the main method to enrich the timeline with new data.
|
||||
* It will call the onUpdated method from [Listener] when the data will be processed.
|
||||
* It will call the onTimelineUpdated method from [Listener] when the data will be processed.
|
||||
* It also ensures only one pagination by direction is launched at a time, so you can safely call this multiple time in a row.
|
||||
*/
|
||||
fun paginate(direction: Direction, count: Int)
|
||||
@ -106,7 +106,12 @@ interface Timeline {
|
||||
* Call when the timeline has been updated through pagination or sync.
|
||||
* @param snapshot the most up to date snapshot
|
||||
*/
|
||||
fun onUpdated(snapshot: List<TimelineEvent>)
|
||||
fun onTimelineUpdated(snapshot: List<TimelineEvent>)
|
||||
|
||||
/**
|
||||
* Called whenever an error we can't recover from occurred
|
||||
*/
|
||||
fun onTimelineFailure(throwable: Throwable)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -44,23 +44,6 @@ data class TimelineEvent(
|
||||
val readReceipts: List<ReadReceipt> = emptyList()
|
||||
) {
|
||||
|
||||
val metadata = HashMap<String, Any>()
|
||||
|
||||
/**
|
||||
* The method to enrich this timeline event.
|
||||
* If you provides multiple data with the same key, only first one will be kept.
|
||||
* @param key the key to associate data with.
|
||||
* @param data the data to enrich with.
|
||||
*/
|
||||
fun enrichWith(key: String?, data: Any?) {
|
||||
if (key == null || data == null) {
|
||||
return
|
||||
}
|
||||
if (!metadata.containsKey(key)) {
|
||||
metadata[key] = data
|
||||
}
|
||||
}
|
||||
|
||||
fun getDisambiguatedDisplayName(): String {
|
||||
return when {
|
||||
senderName.isNullOrBlank() -> root.senderId ?: ""
|
||||
@ -69,15 +52,6 @@ data class TimelineEvent(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the metadata associated with a key.
|
||||
* @param key the key to get the metadata
|
||||
* @return the metadata
|
||||
*/
|
||||
inline fun <reified T> getMetadata(key: String): T? {
|
||||
return metadata[key] as T?
|
||||
}
|
||||
|
||||
fun isEncrypted(): Boolean {
|
||||
// warning: Do not use getClearType here
|
||||
return EventType.ENCRYPTED == root.type
|
||||
@ -104,7 +78,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
|
||||
root.getClearContent().toModel<MessageStickerContent>()
|
||||
} else {
|
||||
annotations?.editSummary?.aggregatedContent?.toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
?: root.getClearContent().toModel()
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,7 +90,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
|
||||
|
||||
if (lastMessageContent != null) {
|
||||
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
|
||||
?: lastMessageContent.body
|
||||
?: lastMessageContent.body
|
||||
}
|
||||
|
||||
return null
|
||||
|
@ -19,99 +19,46 @@ package im.vector.matrix.android.internal.database.helper
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.room.send.SendState
|
||||
import im.vector.matrix.android.internal.database.mapper.asDomain
|
||||
import im.vector.matrix.android.internal.database.mapper.toEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptEntity
|
||||
import im.vector.matrix.android.internal.database.model.ReadReceiptsSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.fastContains
|
||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import io.realm.Sort
|
||||
|
||||
// By default if a chunk is empty we consider it unlinked
|
||||
internal fun ChunkEntity.isUnlinked(): Boolean {
|
||||
assertIsManaged()
|
||||
return timelineEvents.where()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, false)
|
||||
.findAll()
|
||||
.isEmpty()
|
||||
}
|
||||
import io.realm.Realm
|
||||
|
||||
internal fun ChunkEntity.deleteOnCascade() {
|
||||
assertIsManaged()
|
||||
this.stateEvents.deleteAllFromRealm()
|
||||
this.timelineEvents.deleteAllFromRealm()
|
||||
this.deleteFromRealm()
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.merge(roomId: String,
|
||||
chunkToMerge: ChunkEntity,
|
||||
direction: PaginationDirection) {
|
||||
assertIsManaged()
|
||||
val isChunkToMergeUnlinked = chunkToMerge.isUnlinked()
|
||||
val isCurrentChunkUnlinked = this.isUnlinked()
|
||||
val isUnlinked = isCurrentChunkUnlinked && isChunkToMergeUnlinked
|
||||
|
||||
if (isCurrentChunkUnlinked && !isChunkToMergeUnlinked) {
|
||||
this.timelineEvents.forEach { it.root?.isUnlinked = false }
|
||||
}
|
||||
val eventsToMerge: List<TimelineEventEntity>
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
this.nextToken = chunkToMerge.nextToken
|
||||
this.isLastForward = chunkToMerge.isLastForward
|
||||
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.ASCENDING)
|
||||
internal fun ChunkEntity.addStateEvent(stateEvent: Event) {
|
||||
if (stateEvent.eventId == null || stateEvents.fastContains(stateEvent.eventId)) {
|
||||
return
|
||||
} else {
|
||||
this.prevToken = chunkToMerge.prevToken
|
||||
this.isLastBackward = chunkToMerge.isLastBackward
|
||||
eventsToMerge = chunkToMerge.timelineEvents.sort(TimelineEventEntityFields.ROOT.DISPLAY_INDEX, Sort.DESCENDING)
|
||||
}
|
||||
val events = eventsToMerge.mapNotNull { it.root?.asDomain() }
|
||||
val eventIds = ArrayList<String>()
|
||||
events.forEach { event ->
|
||||
add(roomId, event, direction, isUnlinked = isUnlinked)
|
||||
if (event.eventId != null) {
|
||||
eventIds.add(event.eventId)
|
||||
val entity = stateEvent.toEntity(roomId).apply {
|
||||
this.stateIndex = Int.MIN_VALUE
|
||||
this.isUnlinked = true
|
||||
this.sendState = SendState.SYNCED
|
||||
}
|
||||
}
|
||||
updateSenderDataFor(eventIds)
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.addAll(roomId: String,
|
||||
events: List<Event>,
|
||||
direction: PaginationDirection,
|
||||
stateIndexOffset: Int = 0,
|
||||
// Set to true for Event retrieved from a Permalink (i.e. not linked to live Chunk)
|
||||
isUnlinked: Boolean = false) {
|
||||
assertIsManaged()
|
||||
val eventIds = ArrayList<String>()
|
||||
events.forEach { event ->
|
||||
add(roomId, event, direction, stateIndexOffset, isUnlinked)
|
||||
if (event.eventId != null) {
|
||||
eventIds.add(event.eventId)
|
||||
}
|
||||
}
|
||||
updateSenderDataFor(eventIds)
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.updateSenderDataFor(eventIds: List<String>) {
|
||||
for (eventId in eventIds) {
|
||||
val timelineEventEntity = timelineEvents.find(eventId) ?: continue
|
||||
timelineEventEntity.updateSenderData()
|
||||
stateEvents.add(entity)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.add(roomId: String,
|
||||
internal fun ChunkEntity.add(localRealm: Realm,
|
||||
roomId: String,
|
||||
event: Event,
|
||||
direction: PaginationDirection,
|
||||
stateIndexOffset: Int = 0,
|
||||
isUnlinked: Boolean = false) {
|
||||
assertIsManaged()
|
||||
if (event.eventId != null && timelineEvents.find(event.eventId) != null) {
|
||||
if (event.eventId == null || timelineEvents.fastContains(event.eventId)) {
|
||||
return
|
||||
}
|
||||
var currentDisplayIndex = lastDisplayIndex(direction, 0)
|
||||
@ -133,22 +80,21 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
backwardsStateIndex = currentStateIndex
|
||||
}
|
||||
}
|
||||
|
||||
val localId = TimelineEventEntity.nextId(realm)
|
||||
val eventId = event.eventId ?: ""
|
||||
val localId = TimelineEventEntity.nextId(localRealm)
|
||||
val eventId = event.eventId
|
||||
val senderId = event.senderId ?: ""
|
||||
|
||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(realm, eventId).findFirst()
|
||||
val readReceiptsSummaryEntity = ReadReceiptsSummaryEntity.where(localRealm, eventId).findFirst()
|
||||
?: ReadReceiptsSummaryEntity(eventId, roomId)
|
||||
|
||||
// Update RR for the sender of a new message with a dummy one
|
||||
|
||||
if (event.originServerTs != null) {
|
||||
val timestampOfEvent = event.originServerTs.toDouble()
|
||||
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(realm, roomId = roomId, userId = senderId)
|
||||
val readReceiptOfSender = ReadReceiptEntity.getOrCreate(localRealm, roomId = roomId, userId = senderId)
|
||||
// If the synced RR is older, update
|
||||
if (timestampOfEvent > readReceiptOfSender.originServerTs) {
|
||||
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(realm, eventId = readReceiptOfSender.eventId).findFirst()
|
||||
val previousReceiptsSummary = ReadReceiptsSummaryEntity.where(localRealm, eventId = readReceiptOfSender.eventId).findFirst()
|
||||
readReceiptOfSender.eventId = eventId
|
||||
readReceiptOfSender.originServerTs = timestampOfEvent
|
||||
previousReceiptsSummary?.readReceipts?.remove(readReceiptOfSender)
|
||||
@ -165,9 +111,10 @@ internal fun ChunkEntity.add(roomId: String,
|
||||
}
|
||||
it.eventId = eventId
|
||||
it.roomId = roomId
|
||||
it.annotations = EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||
it.annotations = EventAnnotationsSummaryEntity.where(localRealm, eventId).findFirst()
|
||||
it.readReceipts = readReceiptsSummaryEntity
|
||||
}
|
||||
eventEntity.updateSenderData(localRealm, this)
|
||||
val position = if (direction == PaginationDirection.FORWARDS) 0 else this.timelineEvents.size
|
||||
timelineEvents.add(position, eventEntity)
|
||||
}
|
||||
|
@ -26,11 +26,6 @@ import im.vector.matrix.android.internal.database.query.fastContains
|
||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
|
||||
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
|
||||
chunks.remove(chunkEntity)
|
||||
chunkEntity.deleteOnCascade()
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
||||
if (!chunks.contains(chunkEntity)) {
|
||||
chunks.add(chunkEntity)
|
||||
@ -38,21 +33,19 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addStateEvent(stateEvent: Event,
|
||||
stateIndex: Int = Int.MIN_VALUE,
|
||||
filterDuplicates: Boolean = false,
|
||||
isUnlinked: Boolean = false) {
|
||||
assertIsManaged()
|
||||
if (stateEvent.eventId == null || (filterDuplicates && fastContains(stateEvent.eventId))) {
|
||||
stateIndex: Int = Int.MIN_VALUE) {
|
||||
if (stateEvent.eventId == null || untimelinedStateEvents.fastContains(stateEvent.eventId)) {
|
||||
return
|
||||
} else {
|
||||
val entity = stateEvent.toEntity(roomId).apply {
|
||||
this.stateIndex = stateIndex
|
||||
this.isUnlinked = isUnlinked
|
||||
this.isUnlinked = false
|
||||
this.sendState = SendState.SYNCED
|
||||
}
|
||||
untimelinedStateEvents.add(entity)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||
assertIsManaged()
|
||||
val senderId = event.senderId ?: return
|
||||
@ -68,7 +61,6 @@ internal fun RoomEntity.addSendingEvent(event: Event) {
|
||||
it.roomId = roomId
|
||||
it.senderName = myUser?.displayName
|
||||
it.senderAvatar = myUser?.avatarUrl
|
||||
it.isUniqueDisplayName = roomMembers.isUniqueDisplayName(myUser?.displayName)
|
||||
it.senderMembershipEvent = roomMembers.queryRoomMemberEvent(senderId).findFirst()
|
||||
}
|
||||
sendingTimelineEvents.add(0, timelineEventEntity)
|
||||
|
@ -24,42 +24,44 @@ import im.vector.matrix.android.internal.database.model.*
|
||||
import im.vector.matrix.android.internal.database.query.next
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.extensions.assertIsManaged
|
||||
import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmQuery
|
||||
|
||||
internal fun TimelineEventEntity.updateSenderData() {
|
||||
assertIsManaged()
|
||||
internal fun TimelineEventEntity.updateSenderData(realm: Realm, chunkEntity: ChunkEntity) {
|
||||
val roomEntity = RoomEntity.where(realm, roomId = roomId).findFirst() ?: return
|
||||
val stateIndex = root?.stateIndex ?: return
|
||||
val senderId = root?.sender ?: return
|
||||
val chunkEntity = chunk?.firstOrNull() ?: return
|
||||
val isUnlinked = chunkEntity.isUnlinked()
|
||||
var senderMembershipEvent: EventEntity?
|
||||
var senderRoomMemberContent: String?
|
||||
var senderRoomMemberPrevContent: String?
|
||||
when {
|
||||
stateIndex <= 0 -> {
|
||||
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).next(from = stateIndex)?.root
|
||||
senderMembershipEvent = chunkEntity.timelineEvents.buildTimelineEventQuery(senderId).next(from = stateIndex)?.root
|
||||
senderRoomMemberContent = senderMembershipEvent?.prevContent
|
||||
senderRoomMemberPrevContent = senderMembershipEvent?.content
|
||||
}
|
||||
else -> {
|
||||
senderMembershipEvent = chunkEntity.timelineEvents.buildQuery(senderId, isUnlinked).prev(since = stateIndex)?.root
|
||||
senderMembershipEvent = chunkEntity.timelineEvents.buildTimelineEventQuery(senderId).prev(since = stateIndex)?.root
|
||||
senderRoomMemberContent = senderMembershipEvent?.content
|
||||
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
|
||||
}
|
||||
}
|
||||
|
||||
// We fallback to untimelinedStateEvents if we can't find membership events in timeline
|
||||
// We fallback to chunk stateEvents if we can't find membership events in timeline
|
||||
if (senderMembershipEvent == null) {
|
||||
senderMembershipEvent = chunkEntity.stateEvents
|
||||
.buildStateEventQuery(senderId)
|
||||
.prev()
|
||||
senderRoomMemberContent = senderMembershipEvent?.content
|
||||
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
|
||||
}
|
||||
|
||||
// We fallback to room stateEvents if we can't find membership events in timeline and chunk
|
||||
if (senderMembershipEvent == null) {
|
||||
senderMembershipEvent = roomEntity.untimelinedStateEvents
|
||||
.where()
|
||||
.equalTo(EventEntityFields.STATE_KEY, senderId)
|
||||
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
|
||||
.prev(since = stateIndex)
|
||||
.buildStateEventQuery(senderId)
|
||||
.prev()
|
||||
senderRoomMemberContent = senderMembershipEvent?.content
|
||||
senderRoomMemberPrevContent = senderMembershipEvent?.prevContent
|
||||
}
|
||||
@ -67,7 +69,6 @@ internal fun TimelineEventEntity.updateSenderData() {
|
||||
ContentMapper.map(senderRoomMemberContent).toModel<RoomMember>()?.also {
|
||||
this.senderAvatar = it.avatarUrl
|
||||
this.senderName = it.displayName
|
||||
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
|
||||
}
|
||||
|
||||
// We try to fallback on prev content if we got a room member state events with null fields
|
||||
@ -78,7 +79,6 @@ internal fun TimelineEventEntity.updateSenderData() {
|
||||
}
|
||||
if (this.senderName == null && it.displayName != null) {
|
||||
this.senderName = it.displayName
|
||||
this.isUniqueDisplayName = RoomMembers(realm, roomId).isUniqueDisplayName(it.displayName)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,9 +94,14 @@ internal fun TimelineEventEntity.Companion.nextId(realm: Realm): Long {
|
||||
}
|
||||
}
|
||||
|
||||
private fun RealmList<TimelineEventEntity>.buildQuery(sender: String, isUnlinked: Boolean): RealmQuery<TimelineEventEntity> {
|
||||
private fun RealmList<EventEntity>.buildStateEventQuery(sender: String): RealmQuery<EventEntity> {
|
||||
return where()
|
||||
.equalTo(EventEntityFields.STATE_KEY, sender)
|
||||
.equalTo(EventEntityFields.TYPE, EventType.STATE_ROOM_MEMBER)
|
||||
}
|
||||
|
||||
private fun RealmList<TimelineEventEntity>.buildTimelineEventQuery(sender: String): RealmQuery<TimelineEventEntity> {
|
||||
return where()
|
||||
.equalTo(TimelineEventEntityFields.ROOT.STATE_KEY, sender)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.TYPE, EventType.STATE_ROOM_MEMBER)
|
||||
.equalTo(TimelineEventEntityFields.ROOT.IS_UNLINKED, isUnlinked)
|
||||
}
|
||||
|
@ -41,7 +41,8 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
|
||||
localId = timelineEventEntity.localId,
|
||||
displayIndex = timelineEventEntity.root?.displayIndex ?: 0,
|
||||
senderName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
// TODO: handle this properly
|
||||
isUniqueDisplayName = false,
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
readReceipts = readReceipts?.sortedByDescending {
|
||||
it.originServerTs
|
||||
|
@ -24,7 +24,10 @@ import io.realm.annotations.LinkingObjects
|
||||
|
||||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||
@Index var nextToken: String? = null,
|
||||
@Index var roomId: String = "",
|
||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
// These are state events for chunks other than the live one (isLastForward=true)
|
||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||
@Index var isLastForward: Boolean = false,
|
||||
@Index var isLastBackward: Boolean = false,
|
||||
var backwardsDisplayIndex: Int? = null,
|
||||
|
@ -23,6 +23,7 @@ import io.realm.annotations.PrimaryKey
|
||||
|
||||
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
|
||||
var chunks: RealmList<ChunkEntity> = RealmList(),
|
||||
// These are live state events coming from sync only
|
||||
var untimelinedStateEvents: RealmList<EventEntity> = RealmList(),
|
||||
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||
var areAllMembersLoaded: Boolean = false
|
||||
|
@ -27,7 +27,6 @@ internal open class TimelineEventEntity(var localId: Long = 0,
|
||||
var root: EventEntity? = null,
|
||||
var annotations: EventAnnotationsSummaryEntity? = null,
|
||||
var senderName: String? = null,
|
||||
var isUniqueDisplayName: Boolean = false,
|
||||
var senderAvatar: String? = null,
|
||||
var senderMembershipEvent: EventEntity? = null,
|
||||
var readReceipts: ReadReceiptsSummaryEntity? = null
|
||||
|
@ -18,7 +18,6 @@ package im.vector.matrix.android.internal.database.query
|
||||
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntityFields
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
@ -27,7 +26,7 @@ import io.realm.kotlin.where
|
||||
|
||||
internal fun ChunkEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<ChunkEntity> {
|
||||
return realm.where<ChunkEntity>()
|
||||
.equalTo("${ChunkEntityFields.ROOM}.${RoomEntityFields.ROOM_ID}", roomId)
|
||||
.equalTo(ChunkEntityFields.ROOM_ID, roomId)
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity? {
|
||||
@ -57,8 +56,9 @@ internal fun ChunkEntity.Companion.findIncludingEvent(realm: Realm, eventId: Str
|
||||
return findAllIncludingEvents(realm, listOf(eventId)).firstOrNull()
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.Companion.create(realm: Realm, prevToken: String?, nextToken: String?): ChunkEntity {
|
||||
internal fun ChunkEntity.Companion.create(realm: Realm, roomId: String, prevToken: String? = null, nextToken: String? = null): ChunkEntity {
|
||||
return realm.createObject<ChunkEntity>().apply {
|
||||
this.roomId = roomId
|
||||
this.prevToken = prevToken
|
||||
this.nextToken = nextToken
|
||||
}
|
||||
|
@ -115,6 +115,10 @@ internal fun RealmList<TimelineEventEntity>.find(eventId: String): TimelineEvent
|
||||
.findFirst()
|
||||
}
|
||||
|
||||
internal fun RealmList<TimelineEventEntity>.fastContains(eventId: String): Boolean {
|
||||
return find(eventId) != null
|
||||
}
|
||||
|
||||
internal fun TimelineEventEntity.Companion.findAllInRoomWithSendStates(realm: Realm,
|
||||
roomId: String,
|
||||
sendStates: List<SendState>)
|
||||
|
@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.extensions
|
||||
|
||||
import io.realm.RealmObject
|
||||
|
||||
internal fun RealmObject.assertIsManaged() {
|
||||
internal fun RealmObject.assertIsManaged(): Boolean {
|
||||
check(isManaged) { "${javaClass.simpleName} entity should be managed to use this function" }
|
||||
return true
|
||||
}
|
||||
|
@ -122,9 +122,6 @@ internal abstract class RoomModule {
|
||||
@Binds
|
||||
abstract fun bindGetContextOfEventTask(getContextOfEventTask: DefaultGetContextOfEventTask): GetContextOfEventTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindClearUnlinkedEventsTask(clearUnlinkedEventsTask: DefaultClearUnlinkedEventsTask): ClearUnlinkedEventsTask
|
||||
|
||||
@Binds
|
||||
abstract fun bindPaginationTask(paginationTask: DefaultPaginationTask): PaginationTask
|
||||
|
||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.*
|
||||
import im.vector.matrix.android.internal.database.query.isEventRead
|
||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||
import im.vector.matrix.android.internal.database.query.prev
|
||||
@ -36,7 +37,6 @@ import im.vector.matrix.android.internal.session.room.membership.RoomMembers
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncSummary
|
||||
import im.vector.matrix.android.internal.session.sync.model.RoomSyncUnreadNotifications
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId: String,
|
||||
@ -67,9 +67,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||
roomSummary: RoomSyncSummary? = null,
|
||||
unreadNotifications: RoomSyncUnreadNotifications? = null,
|
||||
updateMembers: Boolean = false) {
|
||||
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val roomSummaryEntity = RoomSummaryEntity.getOrCreate(realm, roomId)
|
||||
if (roomSummary != null) {
|
||||
if (roomSummary.heroes.isNotEmpty()) {
|
||||
roomSummaryEntity.heroes.clear()
|
||||
@ -84,17 +82,15 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
|
||||
}
|
||||
roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0
|
||||
roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0
|
||||
|
||||
if (membership != null) {
|
||||
roomSummaryEntity.membership = membership
|
||||
}
|
||||
|
||||
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
|
||||
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
|
||||
|
||||
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
// avoid this call if we are sure there are unread events
|
||||
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
|
||||
|
||||
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
|
||||
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
|
||||
|
@ -19,7 +19,6 @@ package im.vector.matrix.android.internal.session.room.membership
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.helper.updateSenderData
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.network.executeRequest
|
||||
@ -70,9 +69,6 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(private val roomAP
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
roomEntity.chunks.flatMap { it.timelineEvents }.forEach {
|
||||
it.updateSenderData()
|
||||
}
|
||||
roomEntity.areAllMembersLoaded = true
|
||||
roomSummaryUpdater.update(realm, roomId, updateMembers = true)
|
||||
}
|
||||
|
@ -20,12 +20,9 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
||||
import im.vector.matrix.android.api.session.events.model.UnsignedData
|
||||
import im.vector.matrix.android.internal.database.helper.updateSenderData
|
||||
import im.vector.matrix.android.internal.database.mapper.ContentMapper
|
||||
import im.vector.matrix.android.internal.database.mapper.EventMapper
|
||||
import im.vector.matrix.android.internal.database.model.EventEntity
|
||||
import im.vector.matrix.android.internal.database.model.TimelineEventEntity
|
||||
import im.vector.matrix.android.internal.database.query.findWithSenderMembershipEvent
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.di.MoshiProvider
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
@ -94,12 +91,6 @@ internal class DefaultPruneEventTask @Inject constructor(private val monarchy: M
|
||||
// }
|
||||
}
|
||||
}
|
||||
if (eventToPrune.type == EventType.STATE_ROOM_MEMBER) {
|
||||
val timelineEventsToUpdate = TimelineEventEntity.findWithSenderMembershipEvent(realm, eventToPrune.eventId)
|
||||
for (timelineEvent in timelineEventsToUpdate) {
|
||||
timelineEvent.updateSenderData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeAllowedKeys(type: String): List<String> {
|
||||
|
@ -1,48 +0,0 @@
|
||||
/*
|
||||
|
||||
* 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 com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.task.Task
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface ClearUnlinkedEventsTask : Task<ClearUnlinkedEventsTask.Params, Unit> {
|
||||
|
||||
data class Params(val roomId: String)
|
||||
}
|
||||
|
||||
internal class DefaultClearUnlinkedEventsTask @Inject constructor(private val monarchy: Monarchy) : ClearUnlinkedEventsTask {
|
||||
|
||||
override suspend fun execute(params: ClearUnlinkedEventsTask.Params) {
|
||||
monarchy.awaitTransaction { localRealm ->
|
||||
val unlinkedChunks = ChunkEntity
|
||||
.where(localRealm, roomId = params.roomId)
|
||||
.equalTo("${ChunkEntityFields.TIMELINE_EVENTS.ROOT}.${EventEntityFields.IS_UNLINKED}", true)
|
||||
.findAll()
|
||||
unlinkedChunks.forEach {
|
||||
it.deleteOnCascade()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -69,7 +69,6 @@ internal class DefaultTimeline(
|
||||
private val realmConfiguration: RealmConfiguration,
|
||||
private val taskExecutor: TaskExecutor,
|
||||
private val contextOfEventTask: GetContextOfEventTask,
|
||||
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val cryptoService: CryptoService,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
@ -217,9 +216,6 @@ internal class DefaultTimeline(
|
||||
}
|
||||
eventDecryptor.destroy()
|
||||
}
|
||||
clearUnlinkedEventsTask
|
||||
.configureWith(ClearUnlinkedEventsTask.Params(roomId))
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
@ -338,7 +334,7 @@ internal class DefaultTimeline(
|
||||
val lastBuiltEvent = builtEvents.lastOrNull()
|
||||
val firstCacheEvent = results.firstOrNull()
|
||||
val firstBuiltEvent = builtEvents.firstOrNull()
|
||||
val chunkEntity = getLiveChunk()
|
||||
val chunkEntity = getCurrentChunk()
|
||||
|
||||
updateState(Timeline.Direction.FORWARDS) {
|
||||
it.copy(
|
||||
@ -374,7 +370,6 @@ internal class DefaultTimeline(
|
||||
} else {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
}
|
||||
|
||||
return !shouldFetchMore
|
||||
}
|
||||
|
||||
@ -465,13 +460,13 @@ internal class DefaultTimeline(
|
||||
Pair(filteredEvents[range.startIndex]!!.root!!.displayIndex, Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
val state = getState(direction)
|
||||
if (state.isPaginating) {
|
||||
postSnapshot = if (state.isPaginating) {
|
||||
// We are getting new items from pagination
|
||||
postSnapshot = paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount)
|
||||
paginateInternal(startDisplayIndex, direction, state.requestedPaginationCount)
|
||||
} else {
|
||||
// We are getting new items from sync
|
||||
buildTimelineEvents(startDisplayIndex, direction, range.length.toLong())
|
||||
postSnapshot = true
|
||||
true
|
||||
}
|
||||
}
|
||||
changeSet.changes.forEach { index ->
|
||||
@ -491,7 +486,7 @@ internal class DefaultTimeline(
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||
val token = getTokenLive(direction)
|
||||
val token = getCurrentToken(direction)
|
||||
if (token == null) {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
return
|
||||
@ -504,7 +499,6 @@ internal class DefaultTimeline(
|
||||
Timber.v("Should fetch $limit items $direction")
|
||||
cancelableBag += paginationTask
|
||||
.configureWith(params) {
|
||||
this.retryCount = Int.MAX_VALUE
|
||||
this.constraints = TaskConstraints(connectedToNetwork = true)
|
||||
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||
@ -513,6 +507,7 @@ internal class DefaultTimeline(
|
||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||
}
|
||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
postSnapshot()
|
||||
}
|
||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||
@ -524,6 +519,8 @@ internal class DefaultTimeline(
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
postSnapshot()
|
||||
Timber.v("Failure fetching $limit items $direction from pagination request")
|
||||
}
|
||||
}
|
||||
@ -535,15 +532,15 @@ internal class DefaultTimeline(
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
|
||||
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||
val chunkEntity = getLiveChunk() ?: return null
|
||||
private fun getCurrentToken(direction: Timeline.Direction): String? {
|
||||
val chunkEntity = getCurrentChunk() ?: return null
|
||||
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
*/
|
||||
private fun getLiveChunk(): ChunkEntity? {
|
||||
private fun getCurrentChunk(): ChunkEntity? {
|
||||
return filteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||
}
|
||||
|
||||
@ -626,7 +623,7 @@ internal class DefaultTimeline(
|
||||
private fun buildEventQuery(realm: Realm): RealmQuery<TimelineEventEntity> {
|
||||
return if (initialEventId == null) {
|
||||
TimelineEventEntity
|
||||
.where(realm, roomId = roomId, linkFilterMode = EventEntity.LinkFilterMode.LINKED_ONLY)
|
||||
.where(realm, roomId = roomId)
|
||||
.equalTo("${TimelineEventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST_FORWARD}", true)
|
||||
} else {
|
||||
TimelineEventEntity
|
||||
@ -637,7 +634,14 @@ internal class DefaultTimeline(
|
||||
|
||||
private fun fetchEvent(eventId: String) {
|
||||
val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
|
||||
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
|
||||
cancelableBag += contextOfEventTask.configureWith(params) {
|
||||
callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||
override fun onFailure(failure: Throwable) {
|
||||
postFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
|
||||
private fun postSnapshot() {
|
||||
@ -650,7 +654,7 @@ internal class DefaultTimeline(
|
||||
val runnable = Runnable {
|
||||
synchronized(listeners) {
|
||||
listeners.forEach {
|
||||
it.onUpdated(snapshot)
|
||||
it.onTimelineUpdated(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -658,6 +662,20 @@ internal class DefaultTimeline(
|
||||
}
|
||||
}
|
||||
|
||||
private fun postFailure(throwable: Throwable) {
|
||||
if (isReady.get().not()) {
|
||||
return
|
||||
}
|
||||
val runnable = Runnable {
|
||||
synchronized(listeners) {
|
||||
listeners.forEach {
|
||||
it.onTimelineFailure(throwable)
|
||||
}
|
||||
}
|
||||
}
|
||||
mainHandler.post(runnable)
|
||||
}
|
||||
|
||||
private fun clearAllValues() {
|
||||
prevDisplayIndex = null
|
||||
nextDisplayIndex = null
|
||||
|
@ -42,8 +42,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
private val cryptoService: CryptoService,
|
||||
private val paginationTask: PaginationTask,
|
||||
private val timelineEventMapper: TimelineEventMapper,
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
|
||||
private val clearUnlinkedEventsTask: ClearUnlinkedEventsTask
|
||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
||||
) : TimelineService {
|
||||
|
||||
@AssistedInject.Factory
|
||||
@ -57,7 +56,6 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
monarchy.realmConfiguration,
|
||||
taskExecutor,
|
||||
contextOfEventTask,
|
||||
clearUnlinkedEventsTask,
|
||||
paginationTask,
|
||||
cryptoService,
|
||||
timelineEventMapper,
|
||||
@ -69,10 +67,10 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||
override fun getTimeLineEvent(eventId: String): TimelineEvent? {
|
||||
return monarchy
|
||||
.fetchCopyMap({
|
||||
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
|
||||
}, { entity, _ ->
|
||||
timelineEventMapper.map(entity)
|
||||
})
|
||||
TimelineEventEntity.where(it, roomId = roomId, eventId = eventId).findFirst()
|
||||
}, { entity, _ ->
|
||||
timelineEventMapper.map(entity)
|
||||
})
|
||||
}
|
||||
|
||||
override fun getTimeLineEventLive(eventId: String): LiveData<Optional<TimelineEvent>> {
|
||||
|
@ -23,10 +23,11 @@ import im.vector.matrix.android.api.session.events.model.Event
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class EventContextResponse(
|
||||
@Json(name = "event") val event: Event,
|
||||
@Json(name = "start") override val start: String? = null,
|
||||
// Reversed start and end on purpose
|
||||
@Json(name = "start") override val end: String? = null,
|
||||
@Json(name = "end") override val start: String? = null,
|
||||
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(),
|
||||
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
|
||||
@Json(name = "end") override val end: String? = null,
|
||||
@Json(name = "state") override val stateEvents: List<Event> = emptyList()
|
||||
) : TokenChunkEvent {
|
||||
|
||||
|
@ -17,15 +17,19 @@
|
||||
package im.vector.matrix.android.internal.session.room.timeline
|
||||
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
@ -33,7 +37,8 @@ import javax.inject.Inject
|
||||
/**
|
||||
* Insert Chunk in DB, and eventually merge with existing chunk event
|
||||
*/
|
||||
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy) {
|
||||
internal class TokenChunkEventPersistor @Inject constructor(private val monarchy: Monarchy,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater) {
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
@ -107,77 +112,9 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
suspend fun insertInDb(receivedChunk: TokenChunkEvent,
|
||||
roomId: String,
|
||||
direction: PaginationDirection): Result {
|
||||
monarchy
|
||||
.awaitTransaction { realm ->
|
||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
nextToken = receivedChunk.end
|
||||
prevToken = receivedChunk.start
|
||||
} else {
|
||||
nextToken = receivedChunk.start
|
||||
prevToken = receivedChunk.end
|
||||
}
|
||||
|
||||
val shouldSkip = ChunkEntity.find(realm, roomId, nextToken = nextToken) != null
|
||||
|| ChunkEntity.find(realm, roomId, prevToken = prevToken) != null
|
||||
|
||||
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
||||
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
||||
|
||||
// The current chunk is the one we will keep all along the merge processChanges.
|
||||
// We try to look for a chunk next to the token,
|
||||
// otherwise we create a whole new one
|
||||
|
||||
var currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
||||
prevChunk?.apply { this.nextToken = nextToken }
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: ChunkEntity.create(realm, prevToken, nextToken)
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
Timber.v("Reach end of $roomId")
|
||||
currentChunk.isLastBackward = true
|
||||
} else if (!shouldSkip) {
|
||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||
val eventIds = ArrayList<String>(receivedChunk.events.size)
|
||||
for (event in receivedChunk.events) {
|
||||
event.eventId?.also { eventIds.add(it) }
|
||||
currentChunk.add(roomId, event, direction, isUnlinked = currentChunk.isUnlinked())
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
// Then we merge chunks if needed
|
||||
if (currentChunk != prevChunk && prevChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, prevChunk)
|
||||
} else if (currentChunk != nextChunk && nextChunk != null) {
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, nextChunk)
|
||||
} else {
|
||||
val newEventIds = receivedChunk.events.mapNotNull { it.eventId }
|
||||
val overlappedChunks = ChunkEntity.findAllIncludingEvents(realm, newEventIds)
|
||||
overlappedChunks
|
||||
.filter { it != currentChunk }
|
||||
.forEach { overlapped ->
|
||||
currentChunk = handleMerge(roomEntity, direction, currentChunk, overlapped)
|
||||
}
|
||||
}
|
||||
roomEntity.addOrUpdate(currentChunk)
|
||||
for (stateEvent in receivedChunk.stateEvents) {
|
||||
roomEntity.addStateEvent(stateEvent, isUnlinked = currentChunk.isUnlinked())
|
||||
UserEntityFactory.createOrNull(stateEvent)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
currentChunk.updateSenderDataFor(eventIds)
|
||||
}
|
||||
}
|
||||
monarchy.awaitTransaction { realm ->
|
||||
handleChunk(realm, receivedChunk, roomId, direction)
|
||||
}
|
||||
return if (receivedChunk.events.isEmpty()) {
|
||||
if (receivedChunk.start != receivedChunk.end) {
|
||||
Result.SHOULD_FETCH_MORE
|
||||
@ -189,20 +126,74 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMerge(roomEntity: RoomEntity,
|
||||
direction: PaginationDirection,
|
||||
currentChunk: ChunkEntity,
|
||||
otherChunk: ChunkEntity): ChunkEntity {
|
||||
// We always merge the bottom chunk into top chunk, so we are always merging backwards
|
||||
Timber.v("Merge ${currentChunk.prevToken} | ${currentChunk.nextToken} with ${otherChunk.prevToken} | ${otherChunk.nextToken}")
|
||||
return if (direction == PaginationDirection.BACKWARDS && !otherChunk.isLastForward) {
|
||||
currentChunk.merge(roomEntity.roomId, otherChunk, PaginationDirection.BACKWARDS)
|
||||
roomEntity.deleteOnCascade(otherChunk)
|
||||
currentChunk
|
||||
private fun handleChunk(realm: Realm, receivedChunk: TokenChunkEvent, roomId: String, direction: PaginationDirection) {
|
||||
Timber.v("Start persisting ${receivedChunk.events.size} events in $roomId towards $direction")
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
val nextToken: String?
|
||||
val prevToken: String?
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
nextToken = receivedChunk.end
|
||||
prevToken = receivedChunk.start
|
||||
} else {
|
||||
otherChunk.merge(roomEntity.roomId, currentChunk, PaginationDirection.BACKWARDS)
|
||||
roomEntity.deleteOnCascade(currentChunk)
|
||||
otherChunk
|
||||
nextToken = receivedChunk.start
|
||||
prevToken = receivedChunk.end
|
||||
}
|
||||
|
||||
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = prevToken)
|
||||
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = nextToken)
|
||||
|
||||
// We try to look for a chunk next to the token,
|
||||
// otherwise we create a whole new one
|
||||
|
||||
val currentChunk = if (direction == PaginationDirection.FORWARDS) {
|
||||
prevChunk?.apply { this.nextToken = nextToken }
|
||||
} else {
|
||||
nextChunk?.apply { this.prevToken = prevToken }
|
||||
}
|
||||
?: realm.createObject<ChunkEntity>().apply {
|
||||
this.roomId = roomId
|
||||
this.prevToken = prevToken
|
||||
this.nextToken = nextToken
|
||||
}
|
||||
|
||||
// We are saving the state events in the chunk, it will allow us to keep multiple chunks alive
|
||||
receivedChunk.stateEvents.forEach { stateEvent ->
|
||||
currentChunk.addStateEvent(stateEvent)
|
||||
}
|
||||
|
||||
val eventIds = receivedChunk.events.mapNotNull { it.eventId }
|
||||
// If we are overlapping with a chunk other than the live one we remove it to avoid the merging processes
|
||||
ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||
.asSequence()
|
||||
.filterNot { it.isLastForward }
|
||||
.forEach {
|
||||
it.deleteOnCascade()
|
||||
}
|
||||
|
||||
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
|
||||
if (direction == PaginationDirection.FORWARDS) {
|
||||
Timber.v("Reach live state of $roomId")
|
||||
// We make sure we only have one live chunk
|
||||
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.deleteOnCascade()
|
||||
currentChunk.isLastForward = true
|
||||
currentChunk.nextToken = null
|
||||
currentChunk.timelineEvents.forEach {
|
||||
it.root?.isUnlinked = false
|
||||
}
|
||||
} else {
|
||||
Timber.v("Reach end of $roomId")
|
||||
currentChunk.isLastBackward = true
|
||||
}
|
||||
} else {
|
||||
Timber.v("Add ${receivedChunk.events.size} events in chunk(${currentChunk.nextToken} | ${currentChunk.prevToken}")
|
||||
for (event in receivedChunk.events) {
|
||||
currentChunk.add(realm, roomId, event, direction, isUnlinked = !currentChunk.isLastForward)
|
||||
}
|
||||
}
|
||||
roomEntity.addOrUpdate(currentChunk)
|
||||
roomSummaryUpdater.update(realm, roomId, updateMembers = false)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.sync
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.database.helper.add
|
||||
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
|
||||
import im.vector.matrix.android.internal.database.helper.lastStateIndex
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.create
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import io.realm.Realm
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class ChunkEntityFactory @Inject constructor(private val cryptoService: DefaultCryptoService) {
|
||||
|
||||
fun create(realm: Realm,
|
||||
roomId: String,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true,
|
||||
isInitialSync: Boolean): ChunkEntity {
|
||||
return if (isInitialSync) {
|
||||
initialSyncStrategy(realm, roomId, eventList, prevToken)
|
||||
} else {
|
||||
incrementalSyncStrategy(realm, roomId, eventList, prevToken, isLimited)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialSyncStrategy(realm: Realm,
|
||||
roomId: String,
|
||||
eventList: List<Event>,
|
||||
prevToken: String?): ChunkEntity {
|
||||
val chunkEntity = ChunkEntity.create(realm, roomId, prevToken = prevToken).apply {
|
||||
isLastForward = true
|
||||
}
|
||||
for (event in eventList) {
|
||||
chunkEntity.add(realm, roomId, event, PaginationDirection.FORWARDS)
|
||||
// Give info to crypto module
|
||||
cryptoService.onLiveEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
private fun incrementalSyncStrategy(realm: Realm,
|
||||
roomId: String,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
||||
val stateIndexOffset = lastChunk?.lastStateIndex(PaginationDirection.FORWARDS) ?: 0
|
||||
val chunkEntity = if (isLimited || lastChunk == null) {
|
||||
ChunkEntity.create(realm, roomId, prevToken = prevToken).apply {
|
||||
isLastForward = true
|
||||
}
|
||||
} else {
|
||||
lastChunk
|
||||
}
|
||||
if (lastChunk != chunkEntity) {
|
||||
lastChunk?.deleteOnCascade()
|
||||
}
|
||||
for (event in eventList) {
|
||||
chunkEntity.add(realm, roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
// Give info to crypto module
|
||||
cryptoService.onLiveEvent(roomId, event)
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
val sendingEventEntity = roomEntity?.sendingTimelineEvents?.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
}
|
||||
}
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
return chunkEntity
|
||||
}
|
||||
}
|
@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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.sync
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.database.helper.addOrUpdate
|
||||
import im.vector.matrix.android.internal.database.helper.addStateEvent
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||
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.user.UserEntityFactory
|
||||
import io.realm.Realm
|
||||
import io.realm.kotlin.createObject
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class RoomEntityFactory @Inject constructor(private val cryptoService: DefaultCryptoService,
|
||||
private val readReceiptHandler: ReadReceiptHandler,
|
||||
private val roomTagHandler: RoomTagHandler,
|
||||
private val roomFullyReadHandler: RoomFullyReadHandler,
|
||||
private val chunkEntityFactory: ChunkEntityFactory,
|
||||
private val roomSummaryUpdater: RoomSummaryUpdater) {
|
||||
|
||||
fun create(realm: Realm,
|
||||
roomId: String,
|
||||
roomSync: RoomSync,
|
||||
membership: Membership,
|
||||
isInitialSync: Boolean): RoomEntity {
|
||||
return if (isInitialSync) {
|
||||
initialSyncStrategy(realm, roomId, roomSync, membership)
|
||||
} else {
|
||||
incrementalSyncStrategy(realm, roomId, roomSync, membership)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initialSyncStrategy(realm: Realm, roomId: String, roomSync: RoomSync, membership: Membership): RoomEntity {
|
||||
val roomEntity = realm.createObject<RoomEntity>(roomId).apply {
|
||||
this.membership = membership
|
||||
}
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync = true)
|
||||
}
|
||||
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||
}
|
||||
// State events
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
roomSync.state.events.forEach { event ->
|
||||
roomEntity.addStateEvent(event)
|
||||
// Give info to crypto module
|
||||
cryptoService.onStateEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Timeline events
|
||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||
val chunkEntity = chunkEntityFactory.create(
|
||||
realm,
|
||||
roomId,
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited,
|
||||
isInitialSync = true
|
||||
)
|
||||
roomEntity.chunks.add(chunkEntity)
|
||||
}
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, updateMembers = true)
|
||||
return roomEntity
|
||||
}
|
||||
|
||||
private fun incrementalSyncStrategy(realm: Realm, roomId: String, roomSync: RoomSync, membership: Membership): RoomEntity {
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
handleEphemeral(realm, roomId, roomSync.ephemeral, false)
|
||||
}
|
||||
|
||||
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||
}
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
|
||||
?: realm.createObject(roomId)
|
||||
|
||||
if (roomEntity.membership == Membership.INVITE) {
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
}
|
||||
roomEntity.membership = membership
|
||||
// State event
|
||||
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||
?: Int.MIN_VALUE
|
||||
val untimelinedStateIndex = minStateIndex + 1
|
||||
roomSync.state.events.forEach { event ->
|
||||
roomEntity.addStateEvent(event, stateIndex = untimelinedStateIndex)
|
||||
// Give info to crypto module
|
||||
cryptoService.onStateEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||
val chunkEntity = chunkEntityFactory.create(
|
||||
realm,
|
||||
roomId,
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited,
|
||||
false
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
val hasRoomMember = roomSync.state?.events?.firstOrNull {
|
||||
it.type == EventType.STATE_ROOM_MEMBER
|
||||
} != null || roomSync.timeline?.events?.firstOrNull {
|
||||
it.type == EventType.STATE_ROOM_MEMBER
|
||||
} != null
|
||||
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, updateMembers = hasRoomMember)
|
||||
return roomEntity
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun handleEphemeral(realm: Realm,
|
||||
roomId: String,
|
||||
ephemeral: RoomSyncEphemeral,
|
||||
isInitialSync: Boolean) {
|
||||
for (event in ephemeral.events) {
|
||||
if (event.type != EventType.RECEIPT) continue
|
||||
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
|
||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
for (event in accountData.events) {
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
roomTagHandler.handle(realm, roomId, content)
|
||||
} else if (eventType == EventType.FULLY_READ) {
|
||||
val content = event.getClearContent().toModel<FullyReadContent>()
|
||||
roomFullyReadHandler.handle(realm, roomId, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -19,28 +19,18 @@ package im.vector.matrix.android.internal.session.sync
|
||||
import com.zhuinden.monarchy.Monarchy
|
||||
import im.vector.matrix.android.R
|
||||
import im.vector.matrix.android.api.pushrules.RuleScope
|
||||
import im.vector.matrix.android.api.session.events.model.Event
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.Membership
|
||||
import im.vector.matrix.android.api.session.room.model.tag.RoomTagContent
|
||||
import im.vector.matrix.android.internal.crypto.DefaultCryptoService
|
||||
import im.vector.matrix.android.internal.database.helper.*
|
||||
import im.vector.matrix.android.internal.database.model.ChunkEntity
|
||||
import im.vector.matrix.android.internal.database.model.EventEntityFields
|
||||
import im.vector.matrix.android.internal.database.model.RoomEntity
|
||||
import im.vector.matrix.android.internal.database.query.find
|
||||
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
|
||||
import im.vector.matrix.android.internal.database.query.where
|
||||
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
|
||||
import im.vector.matrix.android.internal.session.mapWithProgress
|
||||
import im.vector.matrix.android.internal.session.notification.DefaultPushRuleService
|
||||
import im.vector.matrix.android.internal.session.notification.ProcessEventForPushTask
|
||||
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
|
||||
import im.vector.matrix.android.internal.session.room.read.FullyReadContent
|
||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
|
||||
import im.vector.matrix.android.internal.session.sync.model.*
|
||||
import im.vector.matrix.android.internal.session.user.UserEntityFactory
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||
@ -55,6 +45,8 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
private val roomTagHandler: RoomTagHandler,
|
||||
private val roomFullyReadHandler: RoomFullyReadHandler,
|
||||
private val cryptoService: DefaultCryptoService,
|
||||
private val roomEntityFactory: RoomEntityFactory,
|
||||
private val chunkEntityFactory: ChunkEntityFactory,
|
||||
private val tokenStore: SyncTokenStore,
|
||||
private val pushRuleService: DefaultPushRuleService,
|
||||
private val processForPushTask: ProcessEventForPushTask,
|
||||
@ -100,7 +92,7 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
}
|
||||
is HandlingStrategy.INVITED ->
|
||||
handlingStrategy.data.mapWithProgress(reporter, R.string.initial_sync_start_importing_account_invited_rooms, 0.1f) {
|
||||
handleInvitedRoom(realm, it.key, it.value)
|
||||
handleInvitedRoom(realm, it.key, it.value, isInitialSync)
|
||||
}
|
||||
|
||||
is HandlingStrategy.LEFT -> {
|
||||
@ -117,65 +109,18 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
roomSync: RoomSync,
|
||||
isInitialSync: Boolean): RoomEntity {
|
||||
Timber.v("Handle join sync for room $roomId")
|
||||
|
||||
if (roomSync.ephemeral != null && roomSync.ephemeral.events.isNotEmpty()) {
|
||||
handleEphemeral(realm, roomId, roomSync.ephemeral, isInitialSync)
|
||||
}
|
||||
|
||||
if (roomSync.accountData != null && roomSync.accountData.events.isNullOrEmpty().not()) {
|
||||
handleRoomAccountDataEvents(realm, roomId, roomSync.accountData)
|
||||
}
|
||||
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
|
||||
if (roomEntity.membership == Membership.INVITE) {
|
||||
roomEntity.chunks.deleteAllFromRealm()
|
||||
}
|
||||
roomEntity.membership = Membership.JOIN
|
||||
|
||||
// State event
|
||||
|
||||
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
|
||||
val minStateIndex = roomEntity.untimelinedStateEvents.where().min(EventEntityFields.STATE_INDEX)?.toInt()
|
||||
?: Int.MIN_VALUE
|
||||
val untimelinedStateIndex = minStateIndex + 1
|
||||
roomSync.state.events.forEach { event ->
|
||||
roomEntity.addStateEvent(event, filterDuplicates = true, stateIndex = untimelinedStateIndex)
|
||||
// Give info to crypto module
|
||||
cryptoService.onStateEvent(roomId, event)
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
|
||||
val chunkEntity = handleTimelineEvents(
|
||||
realm,
|
||||
roomEntity,
|
||||
roomSync.timeline.events,
|
||||
roomSync.timeline.prevToken,
|
||||
roomSync.timeline.limited
|
||||
)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
val hasRoomMember = roomSync.state?.events?.firstOrNull {
|
||||
it.type == EventType.STATE_ROOM_MEMBER
|
||||
} != null || roomSync.timeline?.events?.firstOrNull {
|
||||
it.type == EventType.STATE_ROOM_MEMBER
|
||||
} != null
|
||||
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.JOIN, roomSync.summary, roomSync.unreadNotifications, updateMembers = hasRoomMember)
|
||||
return roomEntity
|
||||
return roomEntityFactory.create(realm, roomId, roomSync, Membership.JOIN, isInitialSync)
|
||||
}
|
||||
|
||||
private fun handleInvitedRoom(realm: Realm,
|
||||
roomId: String,
|
||||
roomSync: InvitedRoomSync): RoomEntity {
|
||||
roomSync: InvitedRoomSync,
|
||||
isInitialSync: Boolean): RoomEntity {
|
||||
Timber.v("Handle invited sync for room $roomId")
|
||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId)
|
||||
roomEntity.membership = Membership.INVITE
|
||||
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
|
||||
val chunkEntity = handleTimelineEvents(realm, roomEntity, roomSync.inviteState.events)
|
||||
val chunkEntity = chunkEntityFactory.create(realm, roomId, roomSync.inviteState.events, isInitialSync = isInitialSync)
|
||||
roomEntity.addOrUpdate(chunkEntity)
|
||||
}
|
||||
val hasRoomMember = roomSync.inviteState?.events?.firstOrNull {
|
||||
@ -195,71 +140,4 @@ internal class RoomSyncHandler @Inject constructor(private val monarchy: Monarch
|
||||
roomSummaryUpdater.update(realm, roomId, Membership.LEAVE, roomSync.summary, roomSync.unreadNotifications)
|
||||
return roomEntity
|
||||
}
|
||||
|
||||
private fun handleTimelineEvents(realm: Realm,
|
||||
roomEntity: RoomEntity,
|
||||
eventList: List<Event>,
|
||||
prevToken: String? = null,
|
||||
isLimited: Boolean = true): ChunkEntity {
|
||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
||||
var stateIndexOffset = 0
|
||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||
lastChunk
|
||||
} else {
|
||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||
}
|
||||
if (isLimited && lastChunk != null) {
|
||||
stateIndexOffset = lastChunk.lastStateIndex(PaginationDirection.FORWARDS)
|
||||
}
|
||||
lastChunk?.isLastForward = false
|
||||
chunkEntity.isLastForward = true
|
||||
|
||||
val eventIds = ArrayList<String>(eventList.size)
|
||||
for (event in eventList) {
|
||||
event.eventId?.also { eventIds.add(it) }
|
||||
chunkEntity.add(roomEntity.roomId, event, PaginationDirection.FORWARDS, stateIndexOffset)
|
||||
// Give info to crypto module
|
||||
cryptoService.onLiveEvent(roomEntity.roomId, event)
|
||||
// Try to remove local echo
|
||||
event.unsignedData?.transactionId?.also {
|
||||
val sendingEventEntity = roomEntity.sendingTimelineEvents.find(it)
|
||||
if (sendingEventEntity != null) {
|
||||
Timber.v("Remove local echo for tx:$it")
|
||||
roomEntity.sendingTimelineEvents.remove(sendingEventEntity)
|
||||
} else {
|
||||
Timber.v("Can't find corresponding local echo for tx:$it")
|
||||
}
|
||||
}
|
||||
UserEntityFactory.createOrNull(event)?.also {
|
||||
realm.insertOrUpdate(it)
|
||||
}
|
||||
}
|
||||
chunkEntity.updateSenderDataFor(eventIds)
|
||||
return chunkEntity
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun handleEphemeral(realm: Realm,
|
||||
roomId: String,
|
||||
ephemeral: RoomSyncEphemeral,
|
||||
isInitialSync: Boolean) {
|
||||
for (event in ephemeral.events) {
|
||||
if (event.type != EventType.RECEIPT) continue
|
||||
val readReceiptContent = event.content as? ReadReceiptContent ?: continue
|
||||
readReceiptHandler.handle(realm, roomId, readReceiptContent, isInitialSync)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleRoomAccountDataEvents(realm: Realm, roomId: String, accountData: RoomSyncAccountData) {
|
||||
for (event in accountData.events) {
|
||||
val eventType = event.getClearType()
|
||||
if (eventType == EventType.TAG) {
|
||||
val content = event.getClearContent().toModel<RoomTagContent>()
|
||||
roomTagHandler.handle(realm, roomId, content)
|
||||
} else if (eventType == EventType.FULLY_READ) {
|
||||
val content = event.getClearContent().toModel<FullyReadContent>()
|
||||
roomFullyReadHandler.handle(realm, roomId, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ import im.vector.matrix.android.api.failure.MatrixError
|
||||
import im.vector.matrix.android.api.util.Cancelable
|
||||
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
|
||||
import im.vector.matrix.android.internal.session.sync.SyncTask
|
||||
import im.vector.matrix.android.internal.task.TaskConstraints
|
||||
import im.vector.matrix.android.internal.task.TaskExecutor
|
||||
import im.vector.matrix.android.internal.task.TaskThread
|
||||
import im.vector.matrix.android.internal.task.configureWith
|
||||
@ -108,6 +109,7 @@ open class SyncService : Service() {
|
||||
.configureWith(params) {
|
||||
callbackThread = TaskThread.SYNC
|
||||
executionThread = TaskThread.SYNC
|
||||
constraints = TaskConstraints(connectedToNetwork = true)
|
||||
callback = object : MatrixCallback<Unit> {
|
||||
override fun onSuccess(data: Unit) {
|
||||
cancelableTask = null
|
||||
|
@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import dagger.BindsInstance
|
||||
import dagger.Component
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.core.preference.UserAvatarPreference
|
||||
import im.vector.riotx.features.MainActivity
|
||||
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
|
||||
@ -78,6 +79,8 @@ interface ScreenComponent {
|
||||
|
||||
fun navigator(): Navigator
|
||||
|
||||
fun errorFormatter(): ErrorFormatter
|
||||
|
||||
fun uiStateRepository(): UiStateRepository
|
||||
|
||||
fun inject(activity: HomeActivity)
|
||||
|
@ -31,9 +31,11 @@ import butterknife.Unbinder
|
||||
import com.airbnb.mvrx.BaseMvRxFragment
|
||||
import com.airbnb.mvrx.MvRx
|
||||
import com.bumptech.glide.util.Util.assertMainThread
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import im.vector.riotx.core.di.DaggerScreenComponent
|
||||
import im.vector.riotx.core.di.HasScreenInjector
|
||||
import im.vector.riotx.core.di.ScreenComponent
|
||||
import im.vector.riotx.core.error.ErrorFormatter
|
||||
import im.vector.riotx.features.navigation.Navigator
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import io.reactivex.disposables.Disposable
|
||||
@ -54,6 +56,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
|
||||
protected lateinit var navigator: Navigator
|
||||
private lateinit var screenComponent: ScreenComponent
|
||||
private lateinit var errorFormatter: ErrorFormatter
|
||||
|
||||
/* ==========================================================================================
|
||||
* View model
|
||||
@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
override fun onAttach(context: Context) {
|
||||
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
|
||||
navigator = screenComponent.navigator()
|
||||
errorFormatter = screenComponent.errorFormatter()
|
||||
viewModelFactory = screenComponent.viewModelFactory()
|
||||
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
|
||||
injectWith(injector())
|
||||
@ -163,6 +167,13 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
|
||||
return this
|
||||
}
|
||||
|
||||
protected fun showErrorInSnackbar(throwable: Throwable) {
|
||||
vectorBaseActivity.coordinatorLayout?.let {
|
||||
Snackbar.make(it, errorFormatter.toHumanReadable(throwable), Snackbar.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/* ==========================================================================================
|
||||
* Toolbar
|
||||
* ========================================================================================== */
|
||||
|
@ -112,6 +112,7 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
import im.vector.riotx.features.share.SharedData
|
||||
import im.vector.riotx.features.themes.ThemeUtils
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.synthetic.main.fragment_room_detail.*
|
||||
import kotlinx.android.synthetic.main.merge_composer_layout.view.*
|
||||
@ -272,6 +273,16 @@ class RoomDetailFragment @Inject constructor(
|
||||
roomDetailViewModel.requestLiveData.observeEvent(this) {
|
||||
displayRoomDetailActionResult(it)
|
||||
}
|
||||
|
||||
roomDetailViewModel.viewEvents
|
||||
.observe()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is RoomDetailViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
|
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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.riotx.features.home.room.detail
|
||||
|
||||
/**
|
||||
* Transient events for RoomDetail
|
||||
*/
|
||||
sealed class RoomDetailViewEvents {
|
||||
data class Failure(val throwable: Throwable) : RoomDetailViewEvents()
|
||||
}
|
@ -27,6 +27,7 @@ import com.squareup.inject.assisted.Assisted
|
||||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.MatrixPatterns
|
||||
import im.vector.matrix.android.api.failure.Failure
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.isImageMessage
|
||||
@ -56,7 +57,9 @@ import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||
import im.vector.riotx.core.utils.DataSource
|
||||
import im.vector.riotx.core.utils.LiveEvent
|
||||
import im.vector.riotx.core.utils.PublishDataSource
|
||||
import im.vector.riotx.core.utils.subscribeLogError
|
||||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
@ -101,6 +104,9 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
|
||||
private var timeline = room.createTimeline(eventId, timelineSettings)
|
||||
|
||||
private val _viewEvents = PublishDataSource<RoomDetailViewEvents>()
|
||||
val viewEvents: DataSource<RoomDetailViewEvents> = _viewEvents
|
||||
|
||||
// Can be used for several actions, for a one shot result
|
||||
private val _requestLiveData = MutableLiveData<LiveEvent<Async<RoomDetailAction>>>()
|
||||
val requestLiveData: LiveData<LiveEvent<Async<RoomDetailAction>>>
|
||||
@ -819,9 +825,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
if (events.isEmpty()) return UnreadState.Unknown
|
||||
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
|
||||
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
|
||||
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
|
||||
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||
if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) {
|
||||
return if (timeline.isLive) {
|
||||
UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
|
||||
} else {
|
||||
UnreadState.Unknown
|
||||
}
|
||||
}
|
||||
for (i in (firstDisplayableEventIndex - 1) downTo 0) {
|
||||
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
|
||||
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
|
||||
@ -857,10 +868,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
}
|
||||
}
|
||||
|
||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
timelineEvents.accept(snapshot)
|
||||
}
|
||||
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// If we have a critical timeline issue, we get back to live.
|
||||
timeline.restartWithEventId(null)
|
||||
_viewEvents.post(RoomDetailViewEvents.Failure(throwable))
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
timeline.dispose()
|
||||
timeline.removeListener(this)
|
||||
|
@ -220,7 +220,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
showingForwardLoader = LoadingItem_()
|
||||
.id("forward_loading_item_$timestamp")
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.FORWARDS)
|
||||
.addWhen(Timeline.Direction.FORWARDS)
|
||||
.addWhenLoading(Timeline.Direction.FORWARDS)
|
||||
|
||||
val timelineModels = getModels()
|
||||
add(timelineModels)
|
||||
@ -230,16 +230,20 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
LoadingItem_()
|
||||
.id("backward_loading_item_$timestamp")
|
||||
.setVisibilityStateChangedListener(Timeline.Direction.BACKWARDS)
|
||||
.addWhen(Timeline.Direction.BACKWARDS)
|
||||
.addWhenLoading(Timeline.Direction.BACKWARDS)
|
||||
}
|
||||
}
|
||||
|
||||
// Timeline.LISTENER ***************************************************************************
|
||||
|
||||
override fun onUpdated(snapshot: List<TimelineEvent>) {
|
||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||
submitSnapshot(snapshot)
|
||||
}
|
||||
|
||||
override fun onTimelineFailure(throwable: Throwable) {
|
||||
// no-op, already handled
|
||||
}
|
||||
|
||||
private fun submitSnapshot(newSnapshot: List<TimelineEvent>) {
|
||||
backgroundHandler.post {
|
||||
inSubmitList = true
|
||||
@ -247,6 +251,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
currentSnapshot = newSnapshot
|
||||
val diffResult = DiffUtil.calculateDiff(diffCallback)
|
||||
diffResult.dispatchUpdatesTo(listUpdateCallback)
|
||||
requestDelayedModelBuild(100)
|
||||
inSubmitList = false
|
||||
}
|
||||
}
|
||||
@ -319,7 +324,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||
/**
|
||||
* Return true if added
|
||||
*/
|
||||
private fun LoadingItem_.addWhen(direction: Timeline.Direction): Boolean {
|
||||
private fun LoadingItem_.addWhenLoading(direction: Timeline.Direction): Boolean {
|
||||
val shouldAdd = timeline?.hasMoreToLoad(direction) ?: false
|
||||
addIf(shouldAdd, this@TimelineEventController)
|
||||
return shouldAdd
|
||||
|
@ -106,7 +106,7 @@ class RoomListFragment @Inject constructor(
|
||||
.subscribe {
|
||||
when (it) {
|
||||
is RoomListViewEvents.SelectRoom -> openSelectedRoom(it)
|
||||
is RoomListViewEvents.Failure -> showError(it)
|
||||
is RoomListViewEvents.Failure -> showErrorInSnackbar(it.throwable)
|
||||
}
|
||||
}
|
||||
.disposeOnDestroyView()
|
||||
|
@ -1,4 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawerLayout"
|
||||
|
Loading…
x
Reference in New Issue
Block a user