Merge pull request #1315 from vector-im/feature/forward_pagination
Feature/forward pagination
This commit is contained in:
commit
43497e0da9
|
@ -10,6 +10,7 @@ Improvements 🙌:
|
||||||
- Add a setting to hide redacted events (#951)
|
- Add a setting to hide redacted events (#951)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
|
- After jump to unread, newer messages are never loaded (#1008)
|
||||||
- Fix issues with FontScale switch (#69, #645)
|
- Fix issues with FontScale switch (#69, #645)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
|
|
|
@ -28,10 +28,10 @@ import im.vector.matrix.android.api.auth.data.LoginFlowResult
|
||||||
import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
import im.vector.matrix.android.api.auth.registration.RegistrationResult
|
||||||
import im.vector.matrix.android.api.session.Session
|
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.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.LocalEcho
|
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.Room
|
import im.vector.matrix.android.api.session.room.Room
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.send.SendState
|
||||||
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.api.session.room.timeline.TimelineSettings
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
@ -117,7 +117,7 @@ class CommonTestHelper(context: Context) {
|
||||||
*/
|
*/
|
||||||
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> {
|
||||||
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
|
||||||
val latch = CountDownLatch(nbOfMessages)
|
val latch = CountDownLatch(1)
|
||||||
val timelineListener = object : Timeline.Listener {
|
val timelineListener = object : Timeline.Listener {
|
||||||
override fun onTimelineFailure(throwable: Throwable) {
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
}
|
}
|
||||||
|
@ -128,7 +128,7 @@ class CommonTestHelper(context: Context) {
|
||||||
|
|
||||||
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
val newMessages = snapshot
|
val newMessages = snapshot
|
||||||
.filter { LocalEcho.isLocalEchoId(it.eventId).not() }
|
.filter { it.root.sendState == SendState.SYNCED }
|
||||||
.filter { it.root.getClearType() == EventType.MESSAGE }
|
.filter { it.root.getClearType() == EventType.MESSAGE }
|
||||||
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
|
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
|
||||||
|
|
||||||
|
@ -144,7 +144,8 @@ class CommonTestHelper(context: Context) {
|
||||||
for (i in 0 until nbOfMessages) {
|
for (i in 0 until nbOfMessages) {
|
||||||
room.sendTextMessage(message + " #" + (i + 1))
|
room.sendTextMessage(message + " #" + (i + 1))
|
||||||
}
|
}
|
||||||
await(latch)
|
// Wait 3 second more per message
|
||||||
|
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages)
|
||||||
timeline.removeListener(timelineListener)
|
timeline.removeListener(timelineListener)
|
||||||
timeline.dispose()
|
timeline.dispose()
|
||||||
|
|
||||||
|
@ -292,6 +293,24 @@ class CommonTestHelper(context: Context) {
|
||||||
return requestFailure!!
|
return requestFailure!!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createEventListener(latch: CountDownLatch, predicate: (List<TimelineEvent>) -> Boolean): Timeline.Listener {
|
||||||
|
return object : Timeline.Listener {
|
||||||
|
override fun onTimelineFailure(throwable: Throwable) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewTimelineEvents(eventIds: List<String>) {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
|
||||||
|
if (predicate(snapshot)) {
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Await for a latch and ensure the result is true
|
* Await for a latch and ensure the result is true
|
||||||
*
|
*
|
||||||
|
@ -350,3 +369,13 @@ class CommonTestHelper(context: Context) {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun List<TimelineEvent>.checkSendOrder(baseTextMessage: String, numberOfMessages: Int, startIndex: Int): Boolean {
|
||||||
|
return drop(startIndex)
|
||||||
|
.take(numberOfMessages)
|
||||||
|
.foldRightIndexed(true) { index, timelineEvent, acc ->
|
||||||
|
val body = timelineEvent.root.content.toModel<MessageContent>()?.body
|
||||||
|
val currentMessageSuffix = numberOfMessages - index
|
||||||
|
acc && (body == null || body.startsWith(baseTextMessage) && body.endsWith("#$currentMessageSuffix"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -53,18 +53,20 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
/**
|
/**
|
||||||
* @return alice session
|
* @return alice session
|
||||||
*/
|
*/
|
||||||
fun doE2ETestWithAliceInARoom(): CryptoTestData {
|
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||||
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
|
||||||
|
|
||||||
val roomId = mTestHelper.doSync<String> {
|
val roomId = mTestHelper.doSync<String> {
|
||||||
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (encryptedRoom) {
|
||||||
val room = aliceSession.getRoom(roomId)!!
|
val room = aliceSession.getRoom(roomId)!!
|
||||||
|
|
||||||
mTestHelper.doSync<Unit> {
|
mTestHelper.doSync<Unit> {
|
||||||
room.enableEncryption(callback = it)
|
room.enableEncryption(callback = it)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return CryptoTestData(aliceSession, roomId)
|
return CryptoTestData(aliceSession, roomId)
|
||||||
}
|
}
|
||||||
|
@ -72,8 +74,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
|
||||||
/**
|
/**
|
||||||
* @return alice and bob sessions
|
* @return alice and bob sessions
|
||||||
*/
|
*/
|
||||||
fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData {
|
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
|
||||||
val cryptoTestData = doE2ETestWithAliceInARoom()
|
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
|
||||||
val aliceSession = cryptoTestData.firstSession
|
val aliceSession = cryptoTestData.firstSession
|
||||||
val aliceRoomId = cryptoTestData.roomId
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.session.room.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
|
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.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.CryptoTestHelper
|
||||||
|
import im.vector.matrix.android.common.checkSendOrder
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
|
||||||
|
|
||||||
|
private val commonTestHelper = CommonTestHelper(context())
|
||||||
|
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
|
||||||
|
* even contained in a previous lastForward chunk, we will be able to go back to the live
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun backToPreviousLastForwardTest() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
|
||||||
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||||
|
bobTimeline.start()
|
||||||
|
|
||||||
|
var roomCreationEventId: String? = null
|
||||||
|
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
|
||||||
|
// Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
|
||||||
|
snapshot.size == 8
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob stop to sync
|
||||||
|
bobSession.stopSync()
|
||||||
|
|
||||||
|
val messageRoot = "First messages from Alice"
|
||||||
|
|
||||||
|
// Alice sends 30 messages
|
||||||
|
commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
messageRoot,
|
||||||
|
30)
|
||||||
|
|
||||||
|
// Bob start to sync
|
||||||
|
bobSession.startSync(true)
|
||||||
|
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, we have the 10 last messages from Alice.
|
||||||
|
snapshot.size == 10
|
||||||
|
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob navigate to the first event (room creation event), so inside the previous last forward chunk
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
|
||||||
|
snapshot.size == 4
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
|
||||||
|
// Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
|
||||||
|
assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
|
||||||
|
|
||||||
|
bobTimeline.restartWithEventId(roomCreationEventId)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob scroll to the future
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob can see the first event of the room (so Back pagination has worked)
|
||||||
|
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||||
|
// 8 for room creation item, and 30 for the forward pagination
|
||||||
|
&& snapshot.size == 38
|
||||||
|
&& snapshot.checkSendOrder(messageRoot, 30, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
|
||||||
|
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
bobTimeline.dispose()
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(commonTestHelper)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.session.room.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
|
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.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.CryptoTestHelper
|
||||||
|
import im.vector.matrix.android.common.checkSendOrder
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class TimelineForwardPaginationTest : InstrumentedTest {
|
||||||
|
|
||||||
|
private val commonTestHelper = CommonTestHelper(context())
|
||||||
|
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test ensure that if we click to permalink, we will be able to go back to the live
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun forwardPaginationTest() {
|
||||||
|
val numberOfMessagesToSend = 90
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
|
||||||
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
// Alice sends X messages
|
||||||
|
val message = "Message from Alice"
|
||||||
|
val sentMessages = commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
message,
|
||||||
|
numberOfMessagesToSend)
|
||||||
|
|
||||||
|
// Alice clear the cache
|
||||||
|
commonTestHelper.doSync<Unit> {
|
||||||
|
aliceSession.clearCache(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And restarts the sync
|
||||||
|
aliceSession.startSync(true)
|
||||||
|
|
||||||
|
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
|
||||||
|
aliceTimeline.start()
|
||||||
|
|
||||||
|
// Alice sees the 10 last message of the room, and can only navigate BACKWARD
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root.content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, we have the 10 last messages of the initial sync
|
||||||
|
snapshot.size == 10
|
||||||
|
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(message).orFalse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the timeline at last sent message
|
||||||
|
aliceTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice navigates to the first message of the room, which is not in its database. A GET /context is performed
|
||||||
|
// Then she can paginate BACKWARD and FORWARD
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root.content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The event is not in db, so it is fetch alone
|
||||||
|
snapshot.size == 1
|
||||||
|
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith("Message from Alice").orFalse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.addListener(aliceEventsListener)
|
||||||
|
|
||||||
|
// Restart the timeline to the first sent event
|
||||||
|
aliceTimeline.restartWithEventId(sentMessages.last().eventId)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice paginates BACKWARD and FORWARD of 50 events each
|
||||||
|
// Then she can only navigate FORWARD
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root.content}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice can see the first event of the room (so Back pagination has worked)
|
||||||
|
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||||
|
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||||
|
&& snapshot.size == 6 + 1 + 50
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.addListener(aliceEventsListener)
|
||||||
|
|
||||||
|
// Restart the timeline to the first sent event
|
||||||
|
// We ask to load event backward and forward
|
||||||
|
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alice paginates once again FORWARD for 50 events
|
||||||
|
// All the timeline is retrieved, she cannot paginate anymore in both direction
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root.content}")
|
||||||
|
}
|
||||||
|
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||||
|
snapshot.size == 6 + numberOfMessagesToSend
|
||||||
|
&& snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.addListener(aliceEventsListener)
|
||||||
|
|
||||||
|
// Ask for a forward pagination
|
||||||
|
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
aliceTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
// The timeline is fully loaded
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
aliceTimeline.dispose()
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(commonTestHelper)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.session.room.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.InstrumentedTest
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
|
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.message.MessageContent
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||||
|
import im.vector.matrix.android.common.CommonTestHelper
|
||||||
|
import im.vector.matrix.android.common.CryptoTestHelper
|
||||||
|
import im.vector.matrix.android.common.checkSendOrder
|
||||||
|
import org.amshove.kluent.shouldBeFalse
|
||||||
|
import org.amshove.kluent.shouldBeTrue
|
||||||
|
import org.junit.FixMethodOrder
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.JUnit4
|
||||||
|
import org.junit.runners.MethodSorters
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.CountDownLatch
|
||||||
|
|
||||||
|
@RunWith(JUnit4::class)
|
||||||
|
@FixMethodOrder(MethodSorters.JVM)
|
||||||
|
class TimelinePreviousLastForwardTest : InstrumentedTest {
|
||||||
|
|
||||||
|
private val commonTestHelper = CommonTestHelper(context())
|
||||||
|
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun previousLastForwardTest() {
|
||||||
|
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||||
|
|
||||||
|
val aliceSession = cryptoTestData.firstSession
|
||||||
|
val bobSession = cryptoTestData.secondSession!!
|
||||||
|
val aliceRoomId = cryptoTestData.roomId
|
||||||
|
|
||||||
|
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||||
|
|
||||||
|
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||||
|
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||||
|
|
||||||
|
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||||
|
bobTimeline.start()
|
||||||
|
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
|
||||||
|
snapshot.size == 8
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob stop to sync
|
||||||
|
bobSession.stopSync()
|
||||||
|
|
||||||
|
val firstMessage = "First messages from Alice"
|
||||||
|
// Alice sends 30 messages
|
||||||
|
val firstMessageFromAliceId = commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
firstMessage,
|
||||||
|
30)
|
||||||
|
.last()
|
||||||
|
.eventId
|
||||||
|
|
||||||
|
// Bob start to sync
|
||||||
|
bobSession.startSync(true)
|
||||||
|
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||||
|
snapshot.size == 10
|
||||||
|
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(firstMessage).orFalse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob stop to sync
|
||||||
|
bobSession.stopSync()
|
||||||
|
|
||||||
|
val secondMessage = "Second messages from Alice"
|
||||||
|
// Alice sends again 30 messages
|
||||||
|
commonTestHelper.sendTextMessage(
|
||||||
|
roomFromAlicePOV,
|
||||||
|
secondMessage,
|
||||||
|
30)
|
||||||
|
|
||||||
|
// Bob start to sync
|
||||||
|
bobSession.startSync(true)
|
||||||
|
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||||
|
snapshot.size == 10
|
||||||
|
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(secondMessage).orFalse() }
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob navigate to the first message sent from Alice
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The event is not in db, so it is fetch
|
||||||
|
snapshot.size == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
|
||||||
|
// Restart the timeline to the first sent event, and paginate in both direction
|
||||||
|
bobTimeline.restartWithEventId(firstMessageFromAliceId)
|
||||||
|
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginate in both direction
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.size == 8 + 1 + 35
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
|
||||||
|
// Paginate in both direction
|
||||||
|
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||||
|
// Ensure the chunk in the middle is included in the next pagination
|
||||||
|
bobTimeline.paginate(Timeline.Direction.FORWARDS, 35)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob scroll to the future, till the live
|
||||||
|
run {
|
||||||
|
val lock = CountDownLatch(1)
|
||||||
|
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||||
|
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||||
|
snapshot.forEach {
|
||||||
|
Timber.w(" event ${it.root}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bob can see the first event of the room (so Back pagination has worked)
|
||||||
|
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||||
|
// 8 for room creation item 60 message from Alice
|
||||||
|
&& snapshot.size == 8 + 60
|
||||||
|
&& snapshot.checkSendOrder(secondMessage, 30, 0)
|
||||||
|
&& snapshot.checkSendOrder(firstMessage, 30, 30)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.addListener(eventsListener)
|
||||||
|
|
||||||
|
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||||
|
|
||||||
|
commonTestHelper.await(lock)
|
||||||
|
bobTimeline.removeAllListeners()
|
||||||
|
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||||
|
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||||
|
}
|
||||||
|
|
||||||
|
bobTimeline.dispose()
|
||||||
|
|
||||||
|
cryptoTestData.cleanUp(commonTestHelper)
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ interface Timeline {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the timeline can be enriched by paginating.
|
* Check if the timeline can be enriched by paginating.
|
||||||
* @param the direction to check in
|
* @param direction the direction to check in
|
||||||
* @return true if timeline can be enriched
|
* @return true if timeline can be enriched
|
||||||
*/
|
*/
|
||||||
fun hasMoreToLoad(direction: Direction): Boolean
|
fun hasMoreToLoad(direction: Direction): Boolean
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
package im.vector.matrix.android.api.session.room.timeline
|
package im.vector.matrix.android.api.session.room.timeline
|
||||||
|
|
||||||
|
import im.vector.matrix.android.BuildConfig
|
||||||
import im.vector.matrix.android.api.session.events.model.Event
|
import im.vector.matrix.android.api.session.events.model.Event
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
|
@ -45,6 +46,12 @@ data class TimelineEvent(
|
||||||
val readReceipts: List<ReadReceipt> = emptyList()
|
val readReceipts: List<ReadReceipt> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (BuildConfig.DEBUG) {
|
||||||
|
assert(eventId == root.eventId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val metadata = HashMap<String, Any>()
|
val metadata = HashMap<String, Any>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -200,6 +200,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo3(realm: DynamicRealm) {
|
private fun migrateTo3(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 2 -> 3")
|
||||||
Timber.d("Updating CryptoMetadataEntity table")
|
Timber.d("Updating CryptoMetadataEntity table")
|
||||||
realm.schema.get("CryptoMetadataEntity")
|
realm.schema.get("CryptoMetadataEntity")
|
||||||
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
|
?.addField(CryptoMetadataEntityFields.KEY_BACKUP_RECOVERY_KEY, String::class.java)
|
||||||
|
@ -207,6 +208,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo4(realm: DynamicRealm) {
|
private fun migrateTo4(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 3 -> 4")
|
||||||
Timber.d("Updating KeyInfoEntity table")
|
Timber.d("Updating KeyInfoEntity table")
|
||||||
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
|
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
|
||||||
try {
|
try {
|
||||||
|
@ -238,6 +240,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun migrateTo5(realm: DynamicRealm) {
|
private fun migrateTo5(realm: DynamicRealm) {
|
||||||
|
Timber.d("Step 4 -> 5")
|
||||||
realm.schema.create("MyDeviceLastSeenInfoEntity")
|
realm.schema.create("MyDeviceLastSeenInfoEntity")
|
||||||
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
|
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
|
||||||
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
|
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)
|
||||||
|
|
|
@ -60,8 +60,7 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct
|
||||||
chunkToMerge.stateEvents.forEach { stateEvent ->
|
chunkToMerge.stateEvents.forEach { stateEvent ->
|
||||||
addStateEvent(roomId, stateEvent, direction)
|
addStateEvent(roomId, stateEvent, direction)
|
||||||
}
|
}
|
||||||
return eventsToMerge
|
eventsToMerge.forEach {
|
||||||
.forEach {
|
|
||||||
addTimelineEventFromMerge(localRealm, it, direction)
|
addTimelineEventFromMerge(localRealm, it, direction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,15 +23,20 @@ import io.realm.annotations.Index
|
||||||
import io.realm.annotations.LinkingObjects
|
import io.realm.annotations.LinkingObjects
|
||||||
|
|
||||||
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
internal open class ChunkEntity(@Index var prevToken: String? = null,
|
||||||
|
// Because of gaps we can have several chunks with nextToken == null
|
||||||
@Index var nextToken: String? = null,
|
@Index var nextToken: String? = null,
|
||||||
var stateEvents: RealmList<EventEntity> = RealmList(),
|
var stateEvents: RealmList<EventEntity> = RealmList(),
|
||||||
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
|
||||||
|
// Only one chunk will have isLastForward == true
|
||||||
@Index var isLastForward: Boolean = false,
|
@Index var isLastForward: Boolean = false,
|
||||||
@Index var isLastBackward: Boolean = false
|
@Index var isLastBackward: Boolean = false
|
||||||
) : RealmObject() {
|
) : RealmObject() {
|
||||||
|
|
||||||
fun identifier() = "${prevToken}_$nextToken"
|
fun identifier() = "${prevToken}_$nextToken"
|
||||||
|
|
||||||
|
// If true, then this chunk was previously a last forward chunk
|
||||||
|
fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward
|
||||||
|
|
||||||
@LinkingObjects("chunks")
|
@LinkingObjects("chunks")
|
||||||
val room: RealmResults<RoomEntity>? = null
|
val room: RealmResults<RoomEntity>? = null
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
|
||||||
return query.findFirst()
|
return query.findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun ChunkEntity.Companion.findLastLiveChunkFromRoom(realm: Realm, roomId: String): ChunkEntity? {
|
internal fun ChunkEntity.Companion.findLastForwardChunkOfRoom(realm: Realm, roomId: String): ChunkEntity? {
|
||||||
return where(realm, roomId)
|
return where(realm, roomId)
|
||||||
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
|
||||||
.findFirst()
|
.findFirst()
|
||||||
|
|
|
@ -36,7 +36,7 @@ internal fun isEventRead(monarchy: Monarchy,
|
||||||
var isEventRead = false
|
var isEventRead = false
|
||||||
|
|
||||||
monarchy.doWithRealm { realm ->
|
monarchy.doWithRealm { realm ->
|
||||||
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) ?: return@doWithRealm
|
val liveChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId) ?: return@doWithRealm
|
||||||
val eventToCheck = liveChunk.timelineEvents.find(eventId)
|
val eventToCheck = liveChunk.timelineEvents.find(eventId)
|
||||||
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
|
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
|
||||||
true
|
true
|
||||||
|
|
|
@ -59,7 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
|
||||||
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
|
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
|
||||||
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
|
||||||
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes)
|
val sendingTimelineEvents = roomEntity.sendingTimelineEvents.where().filterTypes(filterTypes)
|
||||||
val liveEvents = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
|
val liveEvents = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)?.timelineEvents?.where()?.filterTypes(filterTypes)
|
||||||
if (filterContentRelation) {
|
if (filterContentRelation) {
|
||||||
liveEvents
|
liveEvents
|
||||||
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
|
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
|
||||||
|
|
|
@ -56,8 +56,10 @@ import im.vector.matrix.android.internal.session.room.reporting.DefaultReportCon
|
||||||
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
|
import im.vector.matrix.android.internal.session.room.reporting.ReportContentTask
|
||||||
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
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultFetchNextTokenAndPaginateTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask
|
||||||
|
import im.vector.matrix.android.internal.session.room.timeline.FetchNextTokenAndPaginateTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
import im.vector.matrix.android.internal.session.room.timeline.GetContextOfEventTask
|
||||||
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
import im.vector.matrix.android.internal.session.room.timeline.PaginationTask
|
||||||
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
import im.vector.matrix.android.internal.session.room.typing.DefaultSendTypingTask
|
||||||
|
@ -143,6 +145,9 @@ internal abstract class RoomModule {
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
|
||||||
|
|
||||||
|
|
|
@ -68,31 +68,40 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
|
||||||
|
|
||||||
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
|
||||||
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
|
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
|
||||||
createLocalEcho(it)
|
.also { createLocalEcho(it) }
|
||||||
|
.let { sendEvent(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// For test only
|
||||||
|
private fun sendTextMessages(text: CharSequence, msgType: String, autoMarkdown: Boolean, times: Int): Cancelable {
|
||||||
|
return CancelableBag().apply {
|
||||||
|
// Send the event several times
|
||||||
|
repeat(times) { i ->
|
||||||
|
localEchoEventFactory.createTextEvent(roomId, msgType, "$text - $i", autoMarkdown)
|
||||||
|
.also { createLocalEcho(it) }
|
||||||
|
.let { sendEvent(it) }
|
||||||
|
.also { add(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
override fun sendFormattedTextMessage(text: String, formattedText: String, msgType: String): Cancelable {
|
||||||
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
|
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
|
||||||
createLocalEcho(it)
|
.also { createLocalEcho(it) }
|
||||||
}
|
.let { sendEvent(it) }
|
||||||
return sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendPoll(question: String, options: List<OptionItem>): Cancelable {
|
override fun sendPoll(question: String, options: List<OptionItem>): Cancelable {
|
||||||
val event = localEchoEventFactory.createPollEvent(roomId, question, options).also {
|
return localEchoEventFactory.createPollEvent(roomId, question, options)
|
||||||
createLocalEcho(it)
|
.also { createLocalEcho(it) }
|
||||||
}
|
.let { sendEvent(it) }
|
||||||
return sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
|
override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
|
||||||
val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also {
|
return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue)
|
||||||
createLocalEcho(it)
|
.also { createLocalEcho(it) }
|
||||||
}
|
.let { sendEvent(it) }
|
||||||
return sendEvent(event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendEvent(event: Event): Cancelable {
|
private fun sendEvent(event: Event): Cancelable {
|
||||||
|
@ -119,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
|
|
||||||
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
override fun redactEvent(event: Event, reason: String?): Cancelable {
|
||||||
// TODO manage media/attachements?
|
// TODO manage media/attachements?
|
||||||
val redactWork = createRedactEventWork(event, reason)
|
return createRedactEventWork(event, reason)
|
||||||
return timelineSendEventWorkCommon.postWork(roomId, redactWork)
|
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
|
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
|
||||||
|
@ -263,31 +272,30 @@ internal class DefaultSendService @AssistedInject constructor(
|
||||||
|
|
||||||
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
// Same parameter
|
// Same parameter
|
||||||
val params = EncryptEventWorker.Params(sessionId, event)
|
return EncryptEventWorker.Params(sessionId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(params)
|
.let { WorkerParamsFactory.toData(it) }
|
||||||
|
.let {
|
||||||
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||||
.setConstraints(WorkManagerProvider.workConstraints)
|
.setConstraints(WorkManagerProvider.workConstraints)
|
||||||
.setInputData(sendWorkData)
|
.setInputData(it)
|
||||||
.startChain(startChain)
|
.startChain(startChain)
|
||||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
|
||||||
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
|
return SendEventWorker.Params(sessionId, event)
|
||||||
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
.let { WorkerParamsFactory.toData(it) }
|
||||||
|
.let { timelineSendEventWorkCommon.createWork<SendEventWorker>(it, startChain) }
|
||||||
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
|
||||||
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
|
return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
|
||||||
createLocalEcho(it)
|
.also { createLocalEcho(it) }
|
||||||
}
|
.let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) }
|
||||||
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
|
.let { WorkerParamsFactory.toData(it) }
|
||||||
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
|
.let { timelineSendEventWorkCommon.createWork<RedactEventWorker>(it, true) }
|
||||||
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
private fun createUploadMediaWork(allLocalEchos: List<Event>,
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
package im.vector.matrix.android.internal.session.room.timeline
|
package im.vector.matrix.android.internal.session.room.timeline
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
|
import im.vector.matrix.android.api.extensions.orFalse
|
||||||
import im.vector.matrix.android.api.session.events.model.EventType
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
import im.vector.matrix.android.api.session.events.model.RelationType
|
import im.vector.matrix.android.api.session.events.model.RelationType
|
||||||
import im.vector.matrix.android.api.session.events.model.toModel
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
@ -71,6 +72,7 @@ internal class DefaultTimeline(
|
||||||
private val realmConfiguration: RealmConfiguration,
|
private val realmConfiguration: RealmConfiguration,
|
||||||
private val taskExecutor: TaskExecutor,
|
private val taskExecutor: TaskExecutor,
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
|
private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val settings: TimelineSettings,
|
private val settings: TimelineSettings,
|
||||||
|
@ -383,7 +385,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
* @return true if createSnapshot should be posted
|
* @return true if createSnapshot should be posted
|
||||||
*/
|
*/
|
||||||
private fun paginateInternal(startDisplayIndex: Int?,
|
private fun paginateInternal(startDisplayIndex: Int?,
|
||||||
|
@ -446,7 +448,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
*/
|
*/
|
||||||
private fun handleInitialLoad() {
|
private fun handleInitialLoad() {
|
||||||
var shouldFetchInitialEvent = false
|
var shouldFetchInitialEvent = false
|
||||||
|
@ -478,7 +480,7 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
*/
|
*/
|
||||||
private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
private fun handleUpdates(results: RealmResults<TimelineEventEntity>, changeSet: OrderedCollectionChangeSet) {
|
||||||
// If changeSet has deletion we are having a gap, so we clear everything
|
// If changeSet has deletion we are having a gap, so we clear everything
|
||||||
|
@ -516,68 +518,90 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
*/
|
*/
|
||||||
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
private fun executePaginationTask(direction: Timeline.Direction, limit: Int) {
|
||||||
val token = getTokenLive(direction)
|
val currentChunk = getLiveChunk()
|
||||||
|
val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) {
|
||||||
|
// We are in the case that next event exists, but we do not know the next token.
|
||||||
|
// Fetch (again) the last event to get a nextToken
|
||||||
|
val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId
|
||||||
|
if (lastKnownEventId == null) {
|
||||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||||
return
|
} else {
|
||||||
}
|
val params = FetchNextTokenAndPaginateTask.Params(
|
||||||
val params = PaginationTask.Params(roomId = roomId,
|
roomId = roomId,
|
||||||
from = token,
|
limit = limit,
|
||||||
direction = direction.toPaginationDirection(),
|
lastKnownEventId = lastKnownEventId
|
||||||
limit = limit)
|
)
|
||||||
|
cancelableBag += fetchNextTokenAndPaginateTask
|
||||||
Timber.v("Should fetch $limit items $direction")
|
|
||||||
cancelableBag += paginationTask
|
|
||||||
.configureWith(params) {
|
.configureWith(params) {
|
||||||
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
this.callback = createPaginationCallback(limit, direction)
|
||||||
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
|
||||||
when (data) {
|
|
||||||
TokenChunkEventPersistor.Result.SUCCESS -> {
|
|
||||||
Timber.v("Success fetching $limit items $direction from pagination request")
|
|
||||||
}
|
|
||||||
TokenChunkEventPersistor.Result.REACHED_END -> {
|
|
||||||
postSnapshot()
|
|
||||||
}
|
|
||||||
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
|
||||||
// Database won't be updated, so we force pagination request
|
|
||||||
BACKGROUND_HANDLER.post {
|
|
||||||
executePaginationTask(direction, limit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(failure: Throwable) {
|
|
||||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
|
||||||
postSnapshot()
|
|
||||||
Timber.v("Failure fetching $limit items $direction from pagination request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.executeBy(taskExecutor)
|
.executeBy(taskExecutor)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val params = PaginationTask.Params(
|
||||||
|
roomId = roomId,
|
||||||
|
from = token,
|
||||||
|
direction = direction.toPaginationDirection(),
|
||||||
|
limit = limit
|
||||||
|
)
|
||||||
|
Timber.v("Should fetch $limit items $direction")
|
||||||
|
cancelableBag += paginationTask
|
||||||
|
.configureWith(params) {
|
||||||
|
this.callback = createPaginationCallback(limit, direction)
|
||||||
|
}
|
||||||
|
.executeBy(taskExecutor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For debug purpose only
|
||||||
|
private fun dumpAndLogChunks() {
|
||||||
|
val liveChunk = getLiveChunk()
|
||||||
|
Timber.w("Live chunk: $liveChunk")
|
||||||
|
|
||||||
|
Realm.getInstance(realmConfiguration).use { realm ->
|
||||||
|
ChunkEntity.where(realm, roomId).findAll()
|
||||||
|
.also { Timber.w("Found ${it.size} chunks") }
|
||||||
|
.forEach {
|
||||||
|
Timber.w("")
|
||||||
|
Timber.w("ChunkEntity: $it")
|
||||||
|
Timber.w("prevToken: ${it.prevToken}")
|
||||||
|
Timber.w("nextToken: ${it.nextToken}")
|
||||||
|
Timber.w("isLastBackward: ${it.isLastBackward}")
|
||||||
|
Timber.w("isLastForward: ${it.isLastForward}")
|
||||||
|
it.timelineEvents.forEach { tle ->
|
||||||
|
Timber.w(" TLE: ${tle.root?.content}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private fun getTokenLive(direction: Timeline.Direction): String? {
|
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||||
val chunkEntity = getLiveChunk() ?: return null
|
val chunkEntity = getLiveChunk() ?: return null
|
||||||
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
|
* Return the current Chunk
|
||||||
*/
|
*/
|
||||||
private fun getLiveChunk(): ChunkEntity? {
|
private fun getLiveChunk(): ChunkEntity? {
|
||||||
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
* @return number of items who have been added
|
* @return the number of items who have been added
|
||||||
*/
|
*/
|
||||||
private fun buildTimelineEvents(startDisplayIndex: Int?,
|
private fun buildTimelineEvents(startDisplayIndex: Int?,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
|
@ -618,6 +642,8 @@ internal class DefaultTimeline(
|
||||||
}
|
}
|
||||||
val time = System.currentTimeMillis() - start
|
val time = System.currentTimeMillis() - start
|
||||||
Timber.v("Built ${offsetResults.size} items from db in $time ms")
|
Timber.v("Built ${offsetResults.size} items from db in $time ms")
|
||||||
|
// For the case where wo reach the lastForward chunk
|
||||||
|
updateLoadingStates(filteredEvents)
|
||||||
return offsetResults.size
|
return offsetResults.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -628,7 +654,7 @@ internal class DefaultTimeline(
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This has to be called on TimelineThread as it access realm live results
|
* This has to be called on TimelineThread as it accesses realm live results
|
||||||
*/
|
*/
|
||||||
private fun getOffsetResults(startDisplayIndex: Int,
|
private fun getOffsetResults(startDisplayIndex: Int,
|
||||||
direction: Timeline.Direction,
|
direction: Timeline.Direction,
|
||||||
|
@ -713,6 +739,32 @@ internal class DefaultTimeline(
|
||||||
forwardsState.set(State())
|
forwardsState.set(State())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createPaginationCallback(limit: Int, direction: Timeline.Direction): MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
|
return object : MatrixCallback<TokenChunkEventPersistor.Result> {
|
||||||
|
override fun onSuccess(data: TokenChunkEventPersistor.Result) {
|
||||||
|
when (data) {
|
||||||
|
TokenChunkEventPersistor.Result.SUCCESS -> {
|
||||||
|
Timber.v("Success fetching $limit items $direction from pagination request")
|
||||||
|
}
|
||||||
|
TokenChunkEventPersistor.Result.REACHED_END -> {
|
||||||
|
postSnapshot()
|
||||||
|
}
|
||||||
|
TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE ->
|
||||||
|
// Database won't be updated, so we force pagination request
|
||||||
|
BACKGROUND_HANDLER.post {
|
||||||
|
executePaginationTask(direction, limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(failure: Throwable) {
|
||||||
|
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||||
|
postSnapshot()
|
||||||
|
Timber.v("Failure fetching $limit items $direction from pagination request")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Extension methods ***************************************************************************
|
// Extension methods ***************************************************************************
|
||||||
|
|
||||||
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
private fun Timeline.Direction.toPaginationDirection(): PaginationDirection {
|
||||||
|
|
|
@ -42,6 +42,7 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||||
private val contextOfEventTask: GetContextOfEventTask,
|
private val contextOfEventTask: GetContextOfEventTask,
|
||||||
private val eventDecryptor: TimelineEventDecryptor,
|
private val eventDecryptor: TimelineEventDecryptor,
|
||||||
private val paginationTask: PaginationTask,
|
private val paginationTask: PaginationTask,
|
||||||
|
private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
|
||||||
private val timelineEventMapper: TimelineEventMapper,
|
private val timelineEventMapper: TimelineEventMapper,
|
||||||
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
|
||||||
) : TimelineService {
|
) : TimelineService {
|
||||||
|
@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
|
||||||
settings = settings,
|
settings = settings,
|
||||||
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
|
||||||
eventBus = eventBus,
|
eventBus = eventBus,
|
||||||
eventDecryptor = eventDecryptor
|
eventDecryptor = eventDecryptor,
|
||||||
|
fetchNextTokenAndPaginateTask = fetchNextTokenAndPaginateTask
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.model.ChunkEntity
|
||||||
|
import im.vector.matrix.android.internal.database.query.findIncludingEvent
|
||||||
|
import im.vector.matrix.android.internal.network.executeRequest
|
||||||
|
import im.vector.matrix.android.internal.session.filter.FilterRepository
|
||||||
|
import im.vector.matrix.android.internal.session.room.RoomAPI
|
||||||
|
import im.vector.matrix.android.internal.task.Task
|
||||||
|
import im.vector.matrix.android.internal.util.awaitTransaction
|
||||||
|
import org.greenrobot.eventbus.EventBus
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface FetchNextTokenAndPaginateTask : Task<FetchNextTokenAndPaginateTask.Params, TokenChunkEventPersistor.Result> {
|
||||||
|
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val lastKnownEventId: String,
|
||||||
|
val limit: Int
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultFetchNextTokenAndPaginateTask @Inject constructor(
|
||||||
|
private val roomAPI: RoomAPI,
|
||||||
|
private val monarchy: Monarchy,
|
||||||
|
private val filterRepository: FilterRepository,
|
||||||
|
private val paginationTask: PaginationTask,
|
||||||
|
private val eventBus: EventBus
|
||||||
|
) : FetchNextTokenAndPaginateTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: FetchNextTokenAndPaginateTask.Params): TokenChunkEventPersistor.Result {
|
||||||
|
val filter = filterRepository.getRoomFilter()
|
||||||
|
val response = executeRequest<EventContextResponse>(eventBus) {
|
||||||
|
apiCall = roomAPI.getContextOfEvent(params.roomId, params.lastKnownEventId, 0, filter)
|
||||||
|
}
|
||||||
|
if (response.end == null) {
|
||||||
|
throw IllegalStateException("No next token found")
|
||||||
|
}
|
||||||
|
monarchy.awaitTransaction {
|
||||||
|
ChunkEntity.findIncludingEvent(it, params.lastKnownEventId)?.nextToken = response.end
|
||||||
|
}
|
||||||
|
val paginationParams = PaginationTask.Params(
|
||||||
|
roomId = params.roomId,
|
||||||
|
from = response.end,
|
||||||
|
direction = PaginationDirection.FORWARDS,
|
||||||
|
limit = params.limit
|
||||||
|
)
|
||||||
|
return paginationTask.execute(paginationParams)
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore
|
||||||
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.findLastLiveChunkFromRoom
|
import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.latestEvent
|
import im.vector.matrix.android.internal.database.query.latestEvent
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
@ -169,10 +169,10 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
private fun handleReachEnd(realm: Realm, roomId: String, direction: PaginationDirection, currentChunk: ChunkEntity) {
|
||||||
Timber.v("Reach end of $roomId")
|
Timber.v("Reach end of $roomId")
|
||||||
if (direction == PaginationDirection.FORWARDS) {
|
if (direction == PaginationDirection.FORWARDS) {
|
||||||
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
|
val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
|
||||||
if (currentChunk != currentLiveChunk) {
|
if (currentChunk != currentLastForwardChunk) {
|
||||||
currentChunk.isLastForward = true
|
currentChunk.isLastForward = true
|
||||||
currentLiveChunk?.deleteOnCascade()
|
currentLastForwardChunk?.deleteOnCascade()
|
||||||
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
|
||||||
latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
latestPreviewableEvent = TimelineEventEntity.latestEvent(
|
||||||
realm,
|
realm,
|
||||||
|
@ -224,10 +224,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
|
|
||||||
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
currentChunk.addTimelineEvent(roomId, eventEntity, direction, roomMemberContentsByUser)
|
||||||
}
|
}
|
||||||
|
// Find all the chunks which contain at least one event from the list of eventIds
|
||||||
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
val chunks = ChunkEntity.findAllIncludingEvents(realm, eventIds)
|
||||||
|
Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
|
||||||
val chunksToDelete = ArrayList<ChunkEntity>()
|
val chunksToDelete = ArrayList<ChunkEntity>()
|
||||||
chunks.forEach {
|
chunks.forEach {
|
||||||
if (it != currentChunk) {
|
if (it != currentChunk) {
|
||||||
|
Timber.d("Merge $it")
|
||||||
currentChunk.merge(roomId, it, direction)
|
currentChunk.merge(roomId, it, direction)
|
||||||
chunksToDelete.add(it)
|
chunksToDelete.add(it)
|
||||||
}
|
}
|
||||||
|
@ -246,6 +249,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
|
||||||
)
|
)
|
||||||
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
|
||||||
}
|
}
|
||||||
|
if (currentChunk.isValid) {
|
||||||
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ import im.vector.matrix.android.internal.database.model.CurrentStateEventEntity
|
||||||
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.copyToRealmOrIgnore
|
import im.vector.matrix.android.internal.database.query.copyToRealmOrIgnore
|
||||||
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.findLastForwardChunkOfRoom
|
||||||
import im.vector.matrix.android.internal.database.query.getOrCreate
|
import im.vector.matrix.android.internal.database.query.getOrCreate
|
||||||
import im.vector.matrix.android.internal.database.query.getOrNull
|
import im.vector.matrix.android.internal.database.query.getOrNull
|
||||||
import im.vector.matrix.android.internal.database.query.where
|
import im.vector.matrix.android.internal.database.query.where
|
||||||
|
@ -220,12 +220,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
|
||||||
prevToken: String? = null,
|
prevToken: String? = null,
|
||||||
isLimited: Boolean = true,
|
isLimited: Boolean = true,
|
||||||
syncLocalTimestampMillis: Long): ChunkEntity {
|
syncLocalTimestampMillis: Long): ChunkEntity {
|
||||||
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
|
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
|
||||||
val chunkEntity = if (!isLimited && lastChunk != null) {
|
val chunkEntity = if (!isLimited && lastChunk != null) {
|
||||||
lastChunk
|
lastChunk
|
||||||
} else {
|
} else {
|
||||||
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
|
||||||
}
|
}
|
||||||
|
// Only one chunk has isLastForward set to true
|
||||||
lastChunk?.isLastForward = false
|
lastChunk?.isLastForward = false
|
||||||
chunkEntity.isLastForward = true
|
chunkEntity.isLastForward = true
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<string name="summary_user_sent_sticker">%1$s sent a sticker.</string>
|
<string name="summary_user_sent_sticker">%1$s sent a sticker.</string>
|
||||||
|
|
||||||
<string name="notice_room_invite_no_invitee">%s\'s invitation</string>
|
<string name="notice_room_invite_no_invitee">%s\'s invitation</string>
|
||||||
|
<string name="notice_room_created">%1$s created the room</string>
|
||||||
<string name="notice_room_invite">%1$s invited %2$s</string>
|
<string name="notice_room_invite">%1$s invited %2$s</string>
|
||||||
<string name="notice_room_invite_you">%1$s invited you</string>
|
<string name="notice_room_invite_you">%1$s invited you</string>
|
||||||
<string name="notice_room_join">%1$s joined the room</string>
|
<string name="notice_room_join">%1$s joined the room</string>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 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.core.extensions
|
||||||
|
|
||||||
|
inline fun <reified T> List<T>.nextOrNull(index: Int) = getOrNull(index + 1)
|
||||||
|
inline fun <reified T> List<T>.prevOrNull(index: Int) = getOrNull(index - 1)
|
|
@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotx.core.date.VectorDateFormatter
|
import im.vector.riotx.core.date.VectorDateFormatter
|
||||||
import im.vector.riotx.core.epoxy.LoadingItem_
|
import im.vector.riotx.core.epoxy.LoadingItem_
|
||||||
import im.vector.riotx.core.extensions.localDateTime
|
import im.vector.riotx.core.extensions.localDateTime
|
||||||
|
import im.vector.riotx.core.extensions.nextOrNull
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailAction
|
import im.vector.riotx.features.home.room.detail.RoomDetailAction
|
||||||
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
|
import im.vector.riotx.features.home.room.detail.RoomDetailViewState
|
||||||
import im.vector.riotx.features.home.room.detail.UnreadState
|
import im.vector.riotx.features.home.room.detail.UnreadState
|
||||||
|
@ -45,7 +46,6 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.ReadMarkerVisib
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
|
|
|
@ -22,7 +22,7 @@ import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.extensions.prevOrNull
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
|
@ -37,15 +37,15 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreatio
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
|
class MergedHeaderItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
|
||||||
private val avatarSizeProvider: AvatarSizeProvider) {
|
private val avatarSizeProvider: AvatarSizeProvider) {
|
||||||
|
|
||||||
private val collapsedEventIds = linkedSetOf<Long>()
|
private val collapsedEventIds = linkedSetOf<Long>()
|
||||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: nextEvent is an older event than event
|
* @param nextEvent is an older event than event
|
||||||
|
* @param items all known items, sorted from newer event to oldest event
|
||||||
*/
|
*/
|
||||||
fun create(event: TimelineEvent,
|
fun create(event: TimelineEvent,
|
||||||
nextEvent: TimelineEvent?,
|
nextEvent: TimelineEvent?,
|
||||||
|
@ -64,8 +64,18 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
buildMembershipEventsMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
event: TimelineEvent,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
|
requestModelBuild: () -> Unit,
|
||||||
|
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
|
||||||
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
||||||
if (prevSameTypeEvents.isEmpty()) {
|
return if (prevSameTypeEvents.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
var highlighted = false
|
var highlighted = false
|
||||||
|
@ -119,7 +129,6 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
||||||
items: List<TimelineEvent>,
|
items: List<TimelineEvent>,
|
||||||
|
@ -127,9 +136,9 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
requestModelBuild: () -> Unit,
|
requestModelBuild: () -> Unit,
|
||||||
callback: TimelineEventController.Callback?): MergedRoomCreationItem_? {
|
callback: TimelineEventController.Callback?): MergedRoomCreationItem_? {
|
||||||
var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null
|
var prevEvent = items.prevOrNull(currentPosition)
|
||||||
var tmpPos = currentPosition - 1
|
var tmpPos = currentPosition - 1
|
||||||
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
|
val mergedEvents = mutableListOf(event)
|
||||||
var hasEncryption = false
|
var hasEncryption = false
|
||||||
var encryptionAlgorithm: String? = null
|
var encryptionAlgorithm: String? = null
|
||||||
while (prevEvent != null && prevEvent.isRoomConfiguration(null)) {
|
while (prevEvent != null && prevEvent.isRoomConfiguration(null)) {
|
||||||
|
@ -139,7 +148,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
}
|
}
|
||||||
mergedEvents.add(prevEvent)
|
mergedEvents.add(prevEvent)
|
||||||
tmpPos--
|
tmpPos--
|
||||||
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
|
prevEvent = items.getOrNull(tmpPos)
|
||||||
}
|
}
|
||||||
return if (mergedEvents.size > 2) {
|
return if (mergedEvents.size > 2) {
|
||||||
var highlighted = false
|
var highlighted = false
|
||||||
|
|
|
@ -21,21 +21,21 @@ import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.resources.ColorProvider
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
import im.vector.riotx.core.resources.StringProvider
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.RoomCreateItem_
|
||||||
import me.gujun.android.span.span
|
import me.gujun.android.span.span
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomCreateItemFactory @Inject constructor(private val colorProvider: ColorProvider,
|
class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider,
|
||||||
private val stringProvider: StringProvider) {
|
private val userPreferencesProvider: UserPreferencesProvider,
|
||||||
|
private val noticeItemFactory: NoticeItemFactory) {
|
||||||
|
|
||||||
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): RoomCreateItem? {
|
fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||||
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>()
|
val createRoomContent = event.root.getClearContent().toModel<RoomCreateContent>() ?: return null
|
||||||
?: return null
|
val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(event, callback)
|
||||||
val predecessorId = createRoomContent.predecessor?.roomId ?: return null
|
|
||||||
val roomLink = PermalinkFactory.createPermalink(predecessorId) ?: return null
|
val roomLink = PermalinkFactory.createPermalink(predecessorId) ?: return null
|
||||||
val text = span {
|
val text = span {
|
||||||
+stringProvider.getString(R.string.room_tombstone_continuation_description)
|
+stringProvider.getString(R.string.room_tombstone_continuation_description)
|
||||||
|
@ -48,4 +48,12 @@ class RoomCreateItemFactory @Inject constructor(private val colorProvider: Color
|
||||||
return RoomCreateItem_()
|
return RoomCreateItem_()
|
||||||
.text(text)
|
.text(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? {
|
||||||
|
return if (userPreferencesProvider.shouldShowHiddenEvents()) {
|
||||||
|
noticeItemFactory.create(event, false, callback)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.room.model.RoomMemberContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
import im.vector.matrix.android.api.session.room.model.RoomNameContent
|
||||||
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
import im.vector.matrix.android.api.session.room.model.RoomTopicContent
|
||||||
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
import im.vector.matrix.android.api.session.room.model.call.CallInviteContent
|
||||||
|
import im.vector.matrix.android.api.session.room.model.create.RoomCreateContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||||
|
@ -47,6 +48,7 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
||||||
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
fun format(timelineEvent: TimelineEvent): CharSequence? {
|
||||||
return when (val type = timelineEvent.root.getClearType()) {
|
return when (val type = timelineEvent.root.getClearType()) {
|
||||||
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
|
EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root)
|
||||||
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
|
||||||
|
@ -98,6 +100,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
|
||||||
return "{ \"type\": ${event.getClearType()} }"
|
return "{ \"type\": ${event.getClearType()} }"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun formatRoomCreateEvent(event: Event): CharSequence? {
|
||||||
|
return event.getClearContent().toModel<RoomCreateContent>()
|
||||||
|
?.takeIf { it.creator.isNullOrBlank().not() }
|
||||||
|
?.let { sp.getString(R.string.notice_room_created, it.creator) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
private fun formatRoomNameEvent(event: Event, senderName: String?): CharSequence? {
|
||||||
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
val content = event.getClearContent().toModel<RoomNameContent>() ?: return null
|
||||||
return if (content.name.isNullOrBlank()) {
|
return if (content.name.isNullOrBlank()) {
|
||||||
|
|
|
@ -106,11 +106,3 @@ fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<Timel
|
||||||
.nextSameTypeEvents(0, minSize)
|
.nextSameTypeEvents(0, minSize)
|
||||||
.reversed()
|
.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun List<TimelineEvent>.nextOrNull(index: Int): TimelineEvent? {
|
|
||||||
return if (index >= size - 1) {
|
|
||||||
null
|
|
||||||
} else {
|
|
||||||
subList(index + 1, this.size).firstOrNull()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue