Merge branch 'develop' into feature/integration_manager

This commit is contained in:
ganfra 2020-05-26 08:55:01 +02:00
commit 7409fde650
147 changed files with 3392 additions and 543 deletions

View file

@ -4,25 +4,32 @@ Changes in RiotX 0.21.0 (2020-XX-XX)
Features ✨: Features ✨:
- Identity server support (#607) - Identity server support (#607)
- Switch language support (#41) - Switch language support (#41)
- Display list of attachments of a room (#860)
Improvements 🙌: Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on - Better connectivity lost indicator when airplane mode is on
- Add a setting to hide redacted events (#951) - Add a setting to hide redacted events (#951)
- Render formatted_body for m.notice and m.emote (#1196)
- Change icon to magnifying-glass to filter room (#1384)
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)
- "Seen by" uses 12h time (#1378)
- Enable markdown (if active) when sending emote (#734)
- Screenshots for Rageshake now includes Dialogs such as BottomSheet (#1349)
Translations 🗣: Translations 🗣:
- -
SDK API changes ⚠️: SDK API changes ⚠️:
- - initialize with proxy configuration
Build 🧱: Build 🧱:
- -
Other changes: Other changes:
- - support new key agreement method for SAS (#1374)
Changes in RiotX 0.20.0 (2020-05-15) Changes in RiotX 0.20.0 (2020-05-15)
=================================================== ===================================================

View file

@ -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"))
}
}

View file

@ -53,17 +53,19 @@ 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)
} }
val room = aliceSession.getRoom(roomId)!! if (encryptedRoom) {
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

View file

@ -468,14 +468,19 @@ class SASTest : InstrumentedTest {
val aliceSASLatch = CountDownLatch(1) val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener { val aliceListener = object : VerificationService.Listener {
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationTransaction).uxState val uxState = (tx as OutgoingSasVerificationTransaction).uxState
Log.v("TEST", "== aliceState ${uxState.name}")
when (uxState) { when (uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> { OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode() tx.userHasVerifiedShortCode()
} }
OutgoingSasVerificationTransaction.UxState.VERIFIED -> { OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
aliceSASLatch.countDown() if (matchOnce) {
matchOnce = false
aliceSASLatch.countDown()
}
} }
else -> Unit else -> Unit
} }
@ -485,14 +490,23 @@ class SASTest : InstrumentedTest {
val bobSASLatch = CountDownLatch(1) val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener { val bobListener = object : VerificationService.Listener {
var acceptOnce = true
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) { override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as IncomingSasVerificationTransaction).uxState val uxState = (tx as IncomingSasVerificationTransaction).uxState
Log.v("TEST", "== bobState ${uxState.name}")
when (uxState) { when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> { IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
tx.performAccept() if (acceptOnce) {
acceptOnce = false
tx.performAccept()
}
} }
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> { IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode() if (matchOnce) {
matchOnce = false
tx.userHasVerifiedShortCode()
}
} }
IncomingSasVerificationTransaction.UxState.VERIFIED -> { IncomingSasVerificationTransaction.UxState.VERIFIED -> {
bobSASLatch.countDown() bobSASLatch.countDown()

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -23,7 +23,6 @@ import androidx.work.WorkManager
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.ElementToDecrypt
import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments import im.vector.matrix.android.internal.crypto.attachments.MXEncryptedAttachments
@ -32,20 +31,10 @@ import im.vector.matrix.android.internal.network.UserAgentHolder
import im.vector.matrix.android.internal.util.BackgroundDetectionObserver import im.vector.matrix.android.internal.util.BackgroundDetectionObserver
import org.matrix.olm.OlmManager import org.matrix.olm.OlmManager
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.Executors import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject import javax.inject.Inject
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig()
) {
interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration
}
}
/** /**
* This is the main entry point to the matrix sdk. * This is the main entry point to the matrix sdk.
* To get the singleton instance, use getInstance static method. * To get the singleton instance, use getInstance static method.

View file

@ -0,0 +1,38 @@
/*
* 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.api
import im.vector.matrix.android.api.crypto.MXCryptoConfig
import java.net.Proxy
data class MatrixConfiguration(
val applicationFlavor: String = "Default-application-flavor",
val cryptoConfig: MXCryptoConfig = MXCryptoConfig(),
/**
* Optional proxy to connect to the matrix servers
* You can create one using for instance Proxy(proxyType, InetSocketAddress(hostname, port)
*/
val proxy: Proxy? = null
) {
/**
* Can be implemented by your Application class
*/
interface Provider {
fun providesMatrixConfiguration(): MatrixConfiguration
}
}

View file

@ -19,11 +19,11 @@ package im.vector.matrix.android.api.crypto
/** /**
* Class to define the parameters used to customize or configure the end-to-end crypto. * Class to define the parameters used to customize or configure the end-to-end crypto.
*/ */
data class MXCryptoConfig( data class MXCryptoConfig constructor(
// Tell whether the encryption of the event content is enabled for the invited members. // Tell whether the encryption of the event content is enabled for the invited members.
// SDK clients can disable this by settings it to false. // SDK clients can disable this by settings it to false.
// Note that the encryption for the invited members will be blocked if the history visibility is "joined". // Note that the encryption for the invited members will be blocked if the history visibility is "joined".
var enableEncryptionForInvitedMembers: Boolean = true, val enableEncryptionForInvitedMembers: Boolean = true,
/** /**
* If set to true, the SDK will automatically ignore room key request (gossiping) * If set to true, the SDK will automatically ignore room key request (gossiping)
@ -31,6 +31,5 @@ data class MXCryptoConfig(
* If set to false, the request will be forwarded to the application layer; in this * If set to false, the request will be forwarded to the application layer; in this
* case the application can decide to prompt the user. * case the application can decide to prompt the user.
*/ */
var discardRoomKeyRequestsFromUntrustedDevices : Boolean = true val discardRoomKeyRequestsFromUntrustedDevices: Boolean = true
) )

View file

@ -220,3 +220,11 @@ fun Event.isImageMessage(): Boolean {
else -> false else -> false
} }
} }
fun Event.isVideoMessage(): Boolean {
return getClearType() == EventType.MESSAGE
&& when (getClearContent()?.toModel<MessageContent>()?.msgType) {
MessageType.MSGTYPE_VIDEO -> true
else -> false
}
}

View file

@ -29,6 +29,7 @@ import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
/** /**
@ -42,6 +43,7 @@ interface Room :
TypingService, TypingService,
MembershipService, MembershipService,
StateService, StateService,
UploadsService,
ReportingService, ReportingService,
RelationService, RelationService,
RoomCryptoService, RoomCryptoService,

View file

@ -0,0 +1,35 @@
/*
* 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.api.session.room.model.message
interface MessageContentWithFormattedBody : MessageContent {
/**
* The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/
val format: String?
/**
* The formatted version of the body. This is required if format is specified.
*/
val formattedBody: String?
/**
* Get the formattedBody, only if not blank and if the format is equal to "org.matrix.custom.html"
*/
val matrixFormattedBody: String?
get() = formattedBody?.takeIf { it.isNotBlank() && format == MessageFormat.FORMAT_MATRIX_HTML }
}

View file

@ -34,15 +34,15 @@ data class MessageEmoteContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageNoticeContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -34,15 +34,15 @@ data class MessageTextContent(
@Json(name = "body") override val body: String, @Json(name = "body") override val body: String,
/** /**
* The format used in the formatted_body. Currently only org.matrix.custom.html is supported. * The format used in the formatted_body. Currently only "org.matrix.custom.html" is supported.
*/ */
@Json(name = "format") val format: String? = null, @Json(name = "format") override val format: String? = null,
/** /**
* The formatted version of the body. This is required if format is specified. * The formatted version of the body. This is required if format is specified.
*/ */
@Json(name = "formatted_body") val formattedBody: String? = null, @Json(name = "formatted_body") override val formattedBody: String? = null,
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null @Json(name = "m.new_content") override val newContent: Content? = null
) : MessageContent ) : MessageContentWithFormattedBody

View file

@ -0,0 +1,34 @@
/*
* 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.api.session.room.sender
data class SenderInfo(
val userId: String,
/**
* Consider using [disambiguatedDisplayName]
*/
val displayName: String?,
val isUniqueDisplayName: Boolean,
val avatarUrl: String?
) {
val disambiguatedDisplayName: String
get() = when {
displayName.isNullOrBlank() -> userId
isUniqueDisplayName -> displayName
else -> "$displayName ($userId)"
}
}

View file

@ -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

View file

@ -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
@ -25,6 +26,7 @@ import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent import im.vector.matrix.android.api.session.room.model.message.MessageStickerContent
import im.vector.matrix.android.api.session.room.model.message.isReply import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply import im.vector.matrix.android.api.util.ContentUtils.extractUsefulTextFromReply
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
@ -38,13 +40,17 @@ data class TimelineEvent(
val localId: Long, val localId: Long,
val eventId: String, val eventId: String,
val displayIndex: Int, val displayIndex: Int,
val senderName: String?, val senderInfo: SenderInfo,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val annotations: EventAnnotationsSummary? = null, val annotations: EventAnnotationsSummary? = null,
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>()
/** /**
@ -62,14 +68,6 @@ data class TimelineEvent(
} }
} }
fun getDisambiguatedDisplayName(): String {
return when {
senderName.isNullOrBlank() -> root.senderId ?: ""
isUniqueDisplayName -> senderName
else -> "$senderName (${root.senderId})"
}
}
/** /**
* Get the metadata associated with a key. * Get the metadata associated with a key.
* @param key the key to get the metadata * @param key the key to get the metadata

View file

@ -0,0 +1,26 @@
/*
* 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.api.session.room.uploads
data class GetUploadsResult(
// List of fetched Events, most recent first
val uploadEvents: List<UploadEvent>,
// token to get more events
val nextToken: String,
// True if there are more event to load
val hasMore: Boolean
)

View file

@ -0,0 +1,32 @@
/*
* 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.api.session.room.uploads
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.sender.SenderInfo
/**
* Wrapper around on Event.
* Similar to [im.vector.matrix.android.api.session.room.timeline.TimelineEvent], contains an Event with extra useful data
*/
data class UploadEvent(
val root: Event,
val eventId: String,
val contentWithAttachmentContent: MessageWithAttachmentContent,
val senderInfo: SenderInfo
)

View file

@ -0,0 +1,35 @@
/*
* 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.api.session.room.uploads
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
/**
* This interface defines methods to get event with uploads (= attachments) sent to a room. It's implemented at the room level.
*/
interface UploadsService {
/**
* Get a list of events containing URL sent to a room, from most recent to oldest one
* @param numberOfEvents the expected number of events to retrieve. The result can contain less events.
* @param since token to get next page, or null to get the first page
*/
fun getUploads(numberOfEvents: Int,
since: String?,
callback: MatrixCallback<GetUploadsResult>): Cancelable
}

View file

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomMemberSummary import im.vector.matrix.android.api.session.room.model.RoomMemberSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import java.util.Locale import java.util.Locale
@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl) fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", avatarUrl)
fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) fun RoomMemberSummary.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun SenderInfo.toMatrixItem() = MatrixItem.UserItem(userId, disambiguatedDisplayName, avatarUrl)

View file

@ -76,7 +76,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } } this.flows?.forEach { it.stages?.mapTo(allFlowTypes) { type -> type } }
allFlowTypes.forEach { type -> allFlowTypes.forEach { type ->
val isMandatory = flows?.all { type in it.stages ?: emptyList() } == true val isMandatory = flows?.all { type in it.stages.orEmpty() } == true
val stage = when (type) { val stage = when (type) {
LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String) LoginFlowTypes.RECAPTCHA -> Stage.ReCaptcha(isMandatory, ((params?.get(type) as? Map<*, *>)?.get("public_key") as? String)
@ -88,7 +88,7 @@ fun RegistrationFlowResponse.toFlowResult(): FlowResult {
else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>)) else -> Stage.Other(isMandatory, type, (params?.get(type) as? Map<*, *>))
} }
if (type in completedStages ?: emptyList()) { if (type in completedStages.orEmpty()) {
completedStage.add(stage) completedStage.add(stage)
} else { } else {
missingStage.add(stage) missingStage.add(stage)

View file

@ -262,7 +262,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun onSuccess(data: DevicesListResponse) { override fun onSuccess(data: DevicesListResponse) {
// Save in local DB // Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList()) cryptoStore.saveMyDevicesInfo(data.devices.orEmpty())
callback.onSuccess(data) callback.onSuccess(data)
} }
} }
@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor(
} }
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> { override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId) ?: emptyList() return cryptoStore.getUserDeviceList(userId).orEmpty()
} }
override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> { override fun getLiveCryptoDeviceInfo(): LiveData<List<CryptoDeviceInfo>> {

View file

@ -137,7 +137,7 @@ internal class OneTimeKeysUploader @Inject constructor(
private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse { private suspend fun uploadOneTimeKeys(oneTimeKeys: Map<String, Map<String, String>>?): KeysUploadResponse {
val oneTimeJson = mutableMapOf<String, Any>() val oneTimeJson = mutableMapOf<String, Any>()
val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY) ?: emptyMap() val curve25519Map = oneTimeKeys?.get(OlmAccount.JSON_KEY_ONE_TIME_KEY).orEmpty()
curve25519Map.forEach { (key_id, value) -> curve25519Map.forEach { (key_id, value) ->
val k = mutableMapOf<String, Any>() val k = mutableMapOf<String, Any>()

View file

@ -34,7 +34,7 @@ internal class EnsureOlmSessionsForUsersAction @Inject constructor(private val o
suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> { suspend fun handle(users: List<String>): MXUsersDevicesMap<MXOlmSessionResult> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users") Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = users.associateWith { userId -> val devicesByUser = users.associateWith { userId ->
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
devices.filter { devices.filter {
// Don't bother setting up session to ourself // Don't bother setting up session to ourself

View file

@ -103,7 +103,7 @@ internal class MXMegolmDecryption(private val userId: String,
senderCurve25519Key = olmDecryptionResult.senderKey, senderCurve25519Key = olmDecryptionResult.senderKey,
claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"), claimedEd25519Key = olmDecryptionResult.keysClaimed?.get("ed25519"),
forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain forwardingCurve25519KeyChain = olmDecryptionResult.forwardingCurve25519KeyChain
?: emptyList() .orEmpty()
) )
} else { } else {
throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON) throw MXCryptoError.Base(MXCryptoError.ErrorType.MISSING_FIELDS, MXCryptoError.MISSING_FIELDS_REASON)

View file

@ -44,7 +44,7 @@ internal class MXOlmEncryption(
ensureSession(userIds) ensureSession(userIds)
val deviceInfos = ArrayList<CryptoDeviceInfo>() val deviceInfos = ArrayList<CryptoDeviceInfo>()
for (userId in userIds) { for (userId in userIds) {
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList() val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
for (device in devices) { for (device in devices) {
val key = device.identityKey() val key = device.identityKey()
if (key == olmDevice.deviceCurve25519Key) { if (key == olmDevice.deviceCurve25519Key) {

View file

@ -47,7 +47,7 @@ internal object CryptoInfoMapper {
return CryptoCrossSigningKey( return CryptoCrossSigningKey(
userId = keyInfo.userId, userId = keyInfo.userId,
usages = keyInfo.usages, usages = keyInfo.usages,
keys = keyInfo.keys ?: emptyMap(), keys = keyInfo.keys.orEmpty(),
signatures = keyInfo.signatures, signatures = keyInfo.signatures,
trustLevel = null trustLevel = null
) )

View file

@ -450,7 +450,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
) )
return Transformations.map(liveData) { return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList() it.firstOrNull().orEmpty()
} }
} }
@ -480,7 +480,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
) )
return Transformations.map(liveData) { return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList() it.firstOrNull().orEmpty()
} }
} }

View file

@ -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)

View file

@ -78,7 +78,7 @@ internal open class OutgoingGossipingRequestEntity(
GossipRequestType.KEY -> { GossipRequestType.KEY -> {
OutgoingRoomKeyRequest( OutgoingRoomKeyRequest(
requestBody = getRequestedKeyInfo(), requestBody = getRequestedKeyInfo(),
recipients = getRecipients() ?: emptyMap(), recipients = getRecipients().orEmpty(),
requestId = requestId ?: "", requestId = requestId ?: "",
state = requestState state = requestState
) )
@ -86,7 +86,7 @@ internal open class OutgoingGossipingRequestEntity(
GossipRequestType.SECRET -> { GossipRequestType.SECRET -> {
OutgoingSecretRequest( OutgoingSecretRequest(
secretName = getRequestedSecretName(), secretName = getRequestedSecretName(),
recipients = getRecipients() ?: emptyMap(), recipients = getRecipients().orEmpty(),
requestId = requestId ?: "", requestId = requestId ?: "",
state = requestState state = requestState
) )

View file

@ -198,18 +198,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// using the result as the shared secret. // using the result as the shared secret.
getSAS().setTheirPublicKey(otherKey) getSAS().setTheirPublicKey(otherKey)
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of: shortCodeBytes = calculateSASBytes()
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
if (BuildConfig.LOG_PRIVATE_DATA) { if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}") Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
@ -219,6 +209,35 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
state = VerificationTxState.ShortCodeReady state = VerificationTxState.ShortCodeReady
} }
private fun calculateSASBytes(): ByteArray {
when (accepted?.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
return getSAS().generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
// Adds the SAS public key, and separate by |
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId"
return getSAS().generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
throw IllegalArgumentException()
}
}
}
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS I: received mac for request id:$transactionId") Timber.v("## SAS I: received mac for request id:$transactionId")
// Check for state? // Check for state?

View file

@ -193,18 +193,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
if (accepted!!.commitment.equals(otherCommitment)) { if (accepted!!.commitment.equals(otherCommitment)) {
getSAS().setTheirPublicKey(otherKey) getSAS().setTheirPublicKey(otherKey)
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function, shortCodeBytes = calculateSASBytes()
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
state = VerificationTxState.ShortCodeReady state = VerificationTxState.ShortCodeReady
} else { } else {
// bad commitment // bad commitment
@ -212,14 +201,45 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
} }
} }
private fun calculateSASBytes(): ByteArray {
when (accepted?.keyAgreementProtocol) {
KEY_AGREEMENT_V1 -> {
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
// - the Matrix ID of the user who sent the m.key.verification.start message,
// - the device ID of the device that sent the m.key.verification.start message,
// - the Matrix ID of the user who sent the m.key.verification.accept message,
// - he device ID of the device that sent the m.key.verification.accept message
// - the transaction ID.
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
// decimal: generate five bytes by using HKDF.
// emoji: generate six bytes by using HKDF.
return getSAS().generateShortCode(sasInfo, 6)
}
KEY_AGREEMENT_V2 -> {
// Adds the SAS public key, and separate by |
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId"
return getSAS().generateShortCode(sasInfo, 6)
}
else -> {
// Protocol has been checked earlier
throw IllegalArgumentException()
}
}
}
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) { override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId") Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
// There is starting to be a huge amount of state / race here :/
if (state != VerificationTxState.OnKeyReceived if (state != VerificationTxState.OnKeyReceived
&& state != VerificationTxState.ShortCodeReady && state != VerificationTxState.ShortCodeReady
&& state != VerificationTxState.ShortCodeAccepted && state != VerificationTxState.ShortCodeAccepted
&& state != VerificationTxState.KeySent
&& state != VerificationTxState.SendingMac && state != VerificationTxState.SendingMac
&& state != VerificationTxState.MacSent) { && state != VerificationTxState.MacSent) {
Timber.e("## SAS O: received key from invalid state $state") Timber.e("## SAS O: received mac from invalid state $state")
cancel(CancelCode.UnexpectedMessage) cancel(CancelCode.UnexpectedMessage)
return return
} }

View file

@ -611,7 +611,7 @@ internal class DefaultVerificationService @Inject constructor(
if (validCancelReq == null) { if (validCancelReq == null) {
// ignore // ignore
Timber.e("## SAS Received invalid key request") Timber.e("## SAS Received invalid cancel request")
// TODO should we cancel? // TODO should we cancel?
return return
} }

View file

@ -26,7 +26,6 @@ import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager import im.vector.matrix.android.internal.crypto.OutgoingGossipingRequestManager
import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction import im.vector.matrix.android.internal.crypto.actions.SetDeviceVerificationAction
import im.vector.matrix.android.internal.crypto.model.MXKey
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.extensions.toUnsignedInt import im.vector.matrix.android.internal.extensions.toUnsignedInt
import im.vector.matrix.android.internal.util.withoutPrefix import im.vector.matrix.android.internal.util.withoutPrefix
@ -66,8 +65,11 @@ internal abstract class SASDefaultVerificationTransaction(
const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256" const val SAS_MAC_SHA256_LONGKDF = "hmac-sha256"
const val SAS_MAC_SHA256 = "hkdf-hmac-sha256" const val SAS_MAC_SHA256 = "hkdf-hmac-sha256"
// Deprecated maybe removed later, use V2
const val KEY_AGREEMENT_V1 = "curve25519"
const val KEY_AGREEMENT_V2 = "curve25519-hkdf-sha256"
// ordered by preferred order // ordered by preferred order
val KNOWN_AGREEMENT_PROTOCOLS = listOf(MXKey.KEY_CURVE_25519_TYPE) val KNOWN_AGREEMENT_PROTOCOLS = listOf(KEY_AGREEMENT_V2, KEY_AGREEMENT_V1)
// ordered by preferred order // ordered by preferred order
val KNOWN_HASHES = listOf("sha256") val KNOWN_HASHES = listOf("sha256")
// ordered by preferred order // ordered by preferred order

View file

@ -60,10 +60,9 @@ 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) }
}
} }
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) { internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {

View file

@ -45,7 +45,7 @@ internal object PushRulesMapper {
private fun fromActionStr(actionsStr: String?): List<Any> { private fun fromActionStr(actionsStr: String?): List<Any> {
try { try {
return actionsStr?.let { moshiActionsAdapter.fromJson(it) } ?: emptyList() return actionsStr?.let { moshiActionsAdapter.fromJson(it) }.orEmpty()
} catch (e: Throwable) { } catch (e: Throwable) {
Timber.e(e, "## failed to map push rule actions <$actionsStr>") Timber.e(e, "## failed to map push rule actions <$actionsStr>")
return emptyList() return emptyList()

View file

@ -49,7 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState, versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(), userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(),
canonicalAlias = roomSummaryEntity.canonicalAlias, canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(), aliases = roomSummaryEntity.aliases.toList(),
isEncrypted = roomSummaryEntity.isEncrypted, isEncrypted = roomSummaryEntity.isEncrypted,

View file

@ -18,7 +18,7 @@ package im.vector.matrix.android.internal.database.mapper
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.ReadReceipt import im.vector.matrix.android.api.session.room.model.ReadReceipt
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.database.model.TimelineEventEntity import im.vector.matrix.android.internal.database.model.TimelineEventEntity
import javax.inject.Inject import javax.inject.Inject
@ -41,15 +41,18 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
annotations = timelineEventEntity.annotations?.asDomain(), annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId, localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex, displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName, senderInfo = SenderInfo(
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName, userId = timelineEventEntity.root?.sender ?: "",
senderAvatar = timelineEventEntity.senderAvatar, displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
avatarUrl = timelineEventEntity.senderAvatar
),
readReceipts = readReceipts readReceipts = readReceipts
?.distinctBy { ?.distinctBy {
it.user it.user
}?.sortedByDescending { }?.sortedByDescending {
it.originServerTs it.originServerTs
} ?: emptyList() }.orEmpty()
) )
} }
} }

View file

@ -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

View file

@ -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()

View file

@ -35,7 +35,7 @@ internal fun FilterEntity.Companion.get(realm: Realm): FilterEntity? {
internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity { internal fun FilterEntity.Companion.getOrCreate(realm: Realm): FilterEntity {
return get(realm) ?: realm.createObject<FilterEntity>() return get(realm) ?: realm.createObject<FilterEntity>()
.apply { .apply {
filterBodyJson = FilterFactory.createDefaultFilterBody().toJSONString() filterBodyJson = FilterFactory.createDefaultFilter().toJSONString()
roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString() roomEventFilterJson = FilterFactory.createDefaultRoomFilter().toJSONString()
filterId = "" filterId = ""
} }

View file

@ -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

View file

@ -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)

View file

@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.MatrixConfiguration
import im.vector.matrix.android.internal.network.TimeOutInterceptor import im.vector.matrix.android.internal.network.TimeOutInterceptor
import im.vector.matrix.android.internal.network.UserAgentInterceptor import im.vector.matrix.android.internal.network.UserAgentInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
@ -64,7 +65,8 @@ internal object NetworkModule {
@Provides @Provides
@JvmStatic @JvmStatic
@Unauthenticated @Unauthenticated
fun providesOkHttpClient(stethoInterceptor: StethoInterceptor, fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration,
stethoInterceptor: StethoInterceptor,
timeoutInterceptor: TimeOutInterceptor, timeoutInterceptor: TimeOutInterceptor,
userAgentInterceptor: UserAgentInterceptor, userAgentInterceptor: UserAgentInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor, httpLoggingInterceptor: HttpLoggingInterceptor,
@ -82,6 +84,9 @@ internal object NetworkModule {
if (BuildConfig.LOG_PRIVATE_DATA) { if (BuildConfig.LOG_PRIVATE_DATA) {
addInterceptor(curlLoggingInterceptor) addInterceptor(curlLoggingInterceptor)
} }
matrixConfiguration.proxy?.let {
proxy(it)
}
} }
.addInterceptor(okReplayInterceptor) .addInterceptor(okReplayInterceptor)
.build() .build()

View file

@ -0,0 +1,24 @@
/*
* 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.extensions
/**
* Ex: "abcdef".subStringBetween("a", "f") -> "bcde"
* Ex: "abcdefff".subStringBetween("a", "f") -> "bcdeff"
* Ex: "aaabcdef".subStringBetween("a", "f") -> "aabcde"
*/
internal fun String.subStringBetween(prefix: String, suffix: String) = substringAfter(prefix).substringBeforeLast(suffix)

View file

@ -229,40 +229,40 @@ internal abstract class SessionModule {
abstract fun bindSession(session: DefaultSession): Session abstract fun bindSession(session: DefaultSession): Session
@Binds @Binds
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindEventsPruner(eventsPruner: EventsPruner): LiveEntityObserver abstract fun bindEventsPruner(pruner: EventsPruner): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): LiveEntityObserver
@Binds @Binds
@IntoSet @IntoSet
abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): LiveEntityObserver
@Binds @Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds @Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
@Binds @Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
@Binds @Binds
abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService abstract fun bindAccountDataService(service: DefaultAccountDataService): AccountDataService

View file

@ -25,8 +25,8 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
internal abstract class ContentModule { internal abstract class ContentModule {
@Binds @Binds
abstract fun bindContentUploadStateTracker(contentUploadStateTracker: DefaultContentUploadStateTracker): ContentUploadStateTracker abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker
@Binds @Binds
abstract fun bindContentUrlResolver(contentUrlResolver: DefaultContentUrlResolver): ContentUrlResolver abstract fun bindContentUrlResolver(resolver: DefaultContentUrlResolver): ContentUrlResolver
} }

View file

@ -28,25 +28,25 @@ import javax.inject.Inject
internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository { internal class DefaultFilterRepository @Inject constructor(private val monarchy: Monarchy) : FilterRepository {
override suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean { override suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean {
return Realm.getInstance(monarchy.realmConfiguration).use { realm -> return Realm.getInstance(monarchy.realmConfiguration).use { realm ->
val filter = FilterEntity.get(realm) val filterEntity = FilterEntity.get(realm)
// Filter has changed, or no filter Id yet // Filter has changed, or no filter Id yet
filter == null filterEntity == null
|| filter.filterBodyJson != filterBody.toJSONString() || filterEntity.filterBodyJson != filter.toJSONString()
|| filter.filterId.isBlank() || filterEntity.filterId.isBlank()
}.also { hasChanged -> }.also { hasChanged ->
if (hasChanged) { if (hasChanged) {
// Filter is new or has changed, store it and reset the filter Id. // Filter is new or has changed, store it and reset the filter Id.
// This has to be done outside of the Realm.use(), because awaitTransaction change the current thread // This has to be done outside of the Realm.use(), because awaitTransaction change the current thread
monarchy.awaitTransaction { realm -> monarchy.awaitTransaction { realm ->
// We manage only one filter for now // We manage only one filter for now
val filterBodyJson = filterBody.toJSONString() val filterJson = filter.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString() val roomEventFilterJson = roomEventFilter.toJSONString()
val filterEntity = FilterEntity.getOrCreate(realm) val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterBodyJson filterEntity.filterBodyJson = filterJson
filterEntity.roomEventFilterJson = roomEventFilterJson filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset filterId // Reset filterId
filterEntity.filterId = "" filterEntity.filterId = ""
@ -55,14 +55,14 @@ internal class DefaultFilterRepository @Inject constructor(private val monarchy:
} }
} }
override suspend fun storeFilterId(filterBody: FilterBody, filterId: String) { override suspend fun storeFilterId(filter: Filter, filterId: String) {
monarchy.awaitTransaction { monarchy.awaitTransaction {
// We manage only one filter for now // We manage only one filter for now
val filterBodyJson = filterBody.toJSONString() val filterJson = filter.toJSONString()
// Update the filter id, only if the filter body matches // Update the filter id, only if the filter body matches
it.where<FilterEntity>() it.where<FilterEntity>()
.equalTo(FilterEntityFields.FILTER_BODY_JSON, filterBodyJson) .equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
?.findFirst() ?.findFirst()
?.filterId = filterId ?.filterId = filterId
} }

View file

@ -43,10 +43,10 @@ internal class DefaultSaveFilterTask @Inject constructor(
override suspend fun execute(params: SaveFilterTask.Params) { override suspend fun execute(params: SaveFilterTask.Params) {
val filterBody = when (params.filterPreset) { val filterBody = when (params.filterPreset) {
FilterService.FilterPreset.RiotFilter -> { FilterService.FilterPreset.RiotFilter -> {
FilterFactory.createRiotFilterBody() FilterFactory.createRiotFilter()
} }
FilterService.FilterPreset.NoFilter -> { FilterService.FilterPreset.NoFilter -> {
FilterFactory.createDefaultFilterBody() FilterFactory.createDefaultFilter()
} }
} }
val roomFilter = when (params.filterPreset) { val roomFilter = when (params.filterPreset) {

View file

@ -0,0 +1,59 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.filter
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
/**
* Represents "Filter" as mentioned in the SPEC
* https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
*/
@JsonClass(generateAdapter = true)
data class EventFilter(
/**
* The maximum number of events to return.
*/
@Json(name = "limit") val limit: Int? = null,
/**
* A list of senders IDs to include. If this list is absent then all senders are included.
*/
@Json(name = "senders") val senders: List<String>? = null,
/**
* A list of sender IDs to exclude. If this list is absent then no senders are excluded.
* A matching sender will be excluded even if it is listed in the 'senders' filter.
*/
@Json(name = "not_senders") val notSenders: List<String>? = null,
/**
* A list of event types to include. If this list is absent then all event types are included.
* A '*' can be used as a wildcard to match any sequence of characters.
*/
@Json(name = "types") val types: List<String>? = null,
/**
* A list of event types to exclude. If this list is absent then no event types are excluded.
* A matching type will be excluded even if it is listed in the 'types' filter.
* A '*' can be used as a wildcard to match any sequence of characters.
*/
@Json(name = "not_types") val notTypes: List<String>? = null
) {
fun hasData(): Boolean {
return limit != null
|| senders != null
|| notSenders != null
|| types != null
|| notTypes != null
}
}

View file

@ -17,28 +17,42 @@ package im.vector.matrix.android.internal.session.filter
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
/** /**
* Represents "Filter" as mentioned in the SPEC * Class which can be parsed to a filter json string. Used for POST and GET
* Have a look here for further information:
* https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter * https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class Filter( internal data class Filter(
@Json(name = "limit") val limit: Int? = null, /**
@Json(name = "senders") val senders: List<String>? = null, * List of event fields to include. If this list is absent then all fields are included. The entries may
@Json(name = "not_senders") val notSenders: List<String>? = null, * include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the
@Json(name = "types") val types: List<String>? = null, * 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may
@Json(name = "not_types") val notTypes: List<String>? = null, * include more fields than were requested.
@Json(name = "rooms") val rooms: List<String>? = null, */
@Json(name = "not_rooms") val notRooms: List<String>? = null @Json(name = "event_fields") val eventFields: List<String>? = null,
/**
* The format to use for events. 'client' will return the events in a format suitable for clients.
* 'federation' will return the raw event as received over federation. The default is 'client'. One of: ["client", "federation"]
*/
@Json(name = "event_format") val eventFormat: String? = null,
/**
* The presence updates to include.
*/
@Json(name = "presence") val presence: EventFilter? = null,
/**
* The user account data that isn't associated with rooms to include.
*/
@Json(name = "account_data") val accountData: EventFilter? = null,
/**
* Filters to be applied to room data.
*/
@Json(name = "room") val room: RoomFilter? = null
) { ) {
fun hasData(): Boolean {
return (limit != null fun toJSONString(): String {
|| senders != null return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this)
|| notSenders != null
|| types != null
|| notTypes != null
|| rooms != null
|| notRooms != null)
} }
} }

View file

@ -32,7 +32,7 @@ internal interface FilterApi {
* @param body the Json representation of a FilterBody object * @param body the Json representation of a FilterBody object
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter")
fun uploadFilter(@Path("userId") userId: String, @Body body: FilterBody): Call<FilterResponse> fun uploadFilter(@Path("userId") userId: String, @Body body: Filter): Call<FilterResponse>
/** /**
* Gets a filter with a given filterId from the homeserver * Gets a filter with a given filterId from the homeserver
@ -42,6 +42,5 @@ internal interface FilterApi {
* @return Filter * @return Filter
*/ */
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}") @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}")
fun getFilterById(@Path("userId") userId: String, @Path("filterId") fun getFilterById(@Path("userId") userId: String, @Path("filterId") filterId: String): Call<Filter>
filterId: String): Call<FilterBody>
} }

View file

@ -1,39 +0,0 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.filter
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.internal.di.MoshiProvider
/**
* Class which can be parsed to a filter json string. Used for POST and GET
* Have a look here for further information:
* https://matrix.org/docs/spec/client_server/r0.3.0.html#post-matrix-client-r0-user-userid-filter
*/
@JsonClass(generateAdapter = true)
internal data class FilterBody(
@Json(name = "event_fields") val eventFields: List<String>? = null,
@Json(name = "event_format") val eventFormat: String? = null,
@Json(name = "presence") val presence: Filter? = null,
@Json(name = "account_data") val accountData: Filter? = null,
@Json(name = "room") val room: RoomFilter? = null
) {
fun toJSONString(): String {
return MoshiProvider.providesMoshi().adapter(FilterBody::class.java).toJson(this)
}
}

View file

@ -20,12 +20,21 @@ import im.vector.matrix.android.api.session.events.model.EventType
internal object FilterFactory { internal object FilterFactory {
fun createDefaultFilterBody(): FilterBody { fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return FilterUtil.enableLazyLoading(FilterBody(), true) return RoomEventFilter(
limit = numberOfEvents,
containsUrl = true,
types = listOf(EventType.MESSAGE),
lazyLoadMembers = true
)
} }
fun createRiotFilterBody(): FilterBody { fun createDefaultFilter(): Filter {
return FilterBody( return FilterUtil.enableLazyLoading(Filter(), true)
}
fun createRiotFilter(): Filter {
return Filter(
room = RoomFilter( room = RoomFilter(
timeline = createRiotTimelineFilter(), timeline = createRiotTimelineFilter(),
state = createRiotStateFilter() state = createRiotStateFilter()

View file

@ -21,12 +21,12 @@ internal interface FilterRepository {
/** /**
* Return true if the filterBody has changed, or need to be sent to the server * Return true if the filterBody has changed, or need to be sent to the server
*/ */
suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
/** /**
* Set the filterId of this filter * Set the filterId of this filter
*/ */
suspend fun storeFilterId(filterBody: FilterBody, filterId: String) suspend fun storeFilterId(filter: Filter, filterId: String)
/** /**
* Return filter json or filter id * Return filter json or filter id

View file

@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class FilterResponse( data class FilterResponse(
/**
* Required. The ID of the filter that was created. Cannot start with a { as this character
* is used to determine if the filter provided is inline JSON or a previously declared
* filter by homeservers on some APIs.
*/
@Json(name = "filter_id") val filterId: String @Json(name = "filter_id") val filterId: String
) )

View file

@ -81,30 +81,30 @@ internal object FilterUtil {
} */ } */
/** /**
* Compute a new filterBody to enable or disable the lazy loading * Compute a new filter to enable or disable the lazy loading
* *
* *
* If lazy loading is on, the filterBody will looks like * If lazy loading is on, the filter will looks like
* {"room":{"state":{"lazy_load_members":true})} * {"room":{"state":{"lazy_load_members":true})}
* *
* @param filterBody filterBody to patch * @param filter filter to patch
* @param useLazyLoading true to enable lazy loading * @param useLazyLoading true to enable lazy loading
*/ */
fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody { fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter {
if (useLazyLoading) { if (useLazyLoading) {
// Enable lazy loading // Enable lazy loading
return filterBody.copy( return filter.copy(
room = filterBody.room?.copy( room = filter.room?.copy(
state = filterBody.room.state?.copy(lazyLoadMembers = true) state = filter.room.state?.copy(lazyLoadMembers = true)
?: RoomEventFilter(lazyLoadMembers = true) ?: RoomEventFilter(lazyLoadMembers = true)
) )
?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true)) ?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true))
) )
} else { } else {
val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() } val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() }
val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() } val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() }
return filterBody.copy( return filter.copy(
room = newRoomFilter room = newRoomFilter
) )
} }

View file

@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomEventFilter( data class RoomEventFilter(
@Json(name = "limit") var limit: Int? = null, /**
* The maximum number of events to return.
*/
@Json(name = "limit") val limit: Int? = null,
/**
* A list of sender IDs to exclude. If this list is absent then no senders are excluded. A matching sender will
* be excluded even if it is listed in the 'senders' filter.
*/
@Json(name = "not_senders") val notSenders: List<String>? = null, @Json(name = "not_senders") val notSenders: List<String>? = null,
/**
* A list of event types to exclude. If this list is absent then no event types are excluded. A matching type will
* be excluded even if it is listed in the 'types' filter. A '*' can be used as a wildcard to match any sequence of characters.
*/
@Json(name = "not_types") val notTypes: List<String>? = null, @Json(name = "not_types") val notTypes: List<String>? = null,
/**
* A list of senders IDs to include. If this list is absent then all senders are included.
*/
@Json(name = "senders") val senders: List<String>? = null, @Json(name = "senders") val senders: List<String>? = null,
/**
* A list of event types to include. If this list is absent then all event types are included. A '*' can be used as
* a wildcard to match any sequence of characters.
*/
@Json(name = "types") val types: List<String>? = null, @Json(name = "types") val types: List<String>? = null,
/**
* A list of room IDs to include. If this list is absent then all rooms are included.
*/
@Json(name = "rooms") val rooms: List<String>? = null, @Json(name = "rooms") val rooms: List<String>? = null,
/**
* A list of room IDs to exclude. If this list is absent then no rooms are excluded. A matching room will be excluded
* even if it is listed in the 'rooms' filter.
*/
@Json(name = "not_rooms") val notRooms: List<String>? = null, @Json(name = "not_rooms") val notRooms: List<String>? = null,
/**
* If true, includes only events with a url key in their content. If false, excludes those events. If omitted, url
* key is not considered for filtering.
*/
@Json(name = "contains_url") val containsUrl: Boolean? = null, @Json(name = "contains_url") val containsUrl: Boolean? = null,
/**
* If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false.
*/
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null @Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
) { ) {

View file

@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class RoomFilter( data class RoomFilter(
/**
* A list of room IDs to exclude. If this list is absent then no rooms are excluded.
* A matching room will be excluded even if it is listed in the 'rooms' filter.
* This filter is applied before the filters in ephemeral, state, timeline or account_data
*/
@Json(name = "not_rooms") val notRooms: List<String>? = null, @Json(name = "not_rooms") val notRooms: List<String>? = null,
/**
* A list of room IDs to include. If this list is absent then all rooms are included.
* This filter is applied before the filters in ephemeral, state, timeline or account_data
*/
@Json(name = "rooms") val rooms: List<String>? = null, @Json(name = "rooms") val rooms: List<String>? = null,
/**
* The events that aren't recorded in the room history, e.g. typing and receipts, to include for rooms.
*/
@Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null, @Json(name = "ephemeral") val ephemeral: RoomEventFilter? = null,
/**
* Include rooms that the user has left in the sync, default false
*/
@Json(name = "include_leave") val includeLeave: Boolean? = null, @Json(name = "include_leave") val includeLeave: Boolean? = null,
/**
* The state events to include for rooms.
* Developer remark: StateFilter is exactly the same than RoomEventFilter
*/
@Json(name = "state") val state: RoomEventFilter? = null, @Json(name = "state") val state: RoomEventFilter? = null,
/**
* The message and state update events to include for rooms.
*/
@Json(name = "timeline") val timeline: RoomEventFilter? = null, @Json(name = "timeline") val timeline: RoomEventFilter? = null,
/**
* The per user account data to include for rooms.
*/
@Json(name = "account_data") val accountData: RoomEventFilter? = null @Json(name = "account_data") val accountData: RoomEventFilter? = null
) { ) {

View file

@ -30,7 +30,7 @@ internal class AccountDataMapper @Inject constructor(moshi: Moshi) {
fun map(entity: UserAccountDataEntity): UserAccountDataEvent { fun map(entity: UserAccountDataEntity): UserAccountDataEvent {
return UserAccountDataEvent( return UserAccountDataEvent(
type = entity.type ?: "", type = entity.type ?: "",
content = entity.contentStr?.let { adapter.fromJson(it) } ?: emptyMap() content = entity.contentStr?.let { adapter.fromJson(it) }.orEmpty()
) )
} }
} }

View file

@ -34,6 +34,7 @@ import im.vector.matrix.android.api.session.room.send.SendService
import im.vector.matrix.android.api.session.room.state.StateService import im.vector.matrix.android.api.session.room.state.StateService
import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.session.room.timeline.TimelineService
import im.vector.matrix.android.api.session.room.typing.TypingService import im.vector.matrix.android.api.session.room.typing.TypingService
import im.vector.matrix.android.api.session.room.uploads.UploadsService
import im.vector.matrix.android.api.util.Optional import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.api.util.toOptional import im.vector.matrix.android.api.util.toOptional
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
@ -54,6 +55,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
private val sendService: SendService, private val sendService: SendService,
private val draftService: DraftService, private val draftService: DraftService,
private val stateService: StateService, private val stateService: StateService,
private val uploadsService: UploadsService,
private val reportingService: ReportingService, private val reportingService: ReportingService,
private val readService: ReadService, private val readService: ReadService,
private val typingService: TypingService, private val typingService: TypingService,
@ -68,6 +70,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
SendService by sendService, SendService by sendService,
DraftService by draftService, DraftService by draftService,
StateService by stateService, StateService by stateService,
UploadsService by uploadsService,
ReportingService by reportingService, ReportingService by reportingService,
ReadService by readService, ReadService by readService,
TypingService by typingService, TypingService by typingService,

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.internal.session.room.state.DefaultStateService
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.DefaultTimelineService import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService
import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService import im.vector.matrix.android.internal.session.room.typing.DefaultTypingService
import im.vector.matrix.android.internal.session.room.uploads.DefaultUploadsService
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import javax.inject.Inject import javax.inject.Inject
@ -47,6 +48,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val sendServiceFactory: DefaultSendService.Factory, private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory, private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory, private val stateServiceFactory: DefaultStateService.Factory,
private val uploadsServiceFactory: DefaultUploadsService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory, private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory, private val readServiceFactory: DefaultReadService.Factory,
private val typingServiceFactory: DefaultTypingService.Factory, private val typingServiceFactory: DefaultTypingService.Factory,
@ -66,6 +68,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendService = sendServiceFactory.create(roomId), sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId), draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId), stateService = stateServiceFactory.create(roomId),
uploadsService = uploadsServiceFactory.create(roomId),
reportingService = reportingServiceFactory.create(roomId), reportingService = reportingServiceFactory.create(roomId),
readService = readServiceFactory.create(roomId), readService = readServiceFactory.create(roomId),
typingService = typingServiceFactory.create(roomId), typingService = typingServiceFactory.create(roomId),

View file

@ -56,12 +56,16 @@ 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
import im.vector.matrix.android.internal.session.room.typing.SendTypingTask import im.vector.matrix.android.internal.session.room.typing.SendTypingTask
import im.vector.matrix.android.internal.session.room.uploads.DefaultGetUploadsTask
import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask
import retrofit2.Retrofit import retrofit2.Retrofit
@Module @Module
@ -143,6 +147,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
@ -151,4 +158,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask abstract fun bindSendTypingTask(task: DefaultSendTypingTask): SendTypingTask
@Binds
abstract fun bindGetUploadsTask(task: DefaultGetUploadsTask): GetUploadsTask
} }

View file

@ -131,7 +131,7 @@ internal class RoomSummaryUpdater @Inject constructor(
?.canonicalAlias ?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList() .orEmpty()
roomSummaryEntity.aliases.clear() roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases) roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|") roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")

View file

@ -143,7 +143,7 @@ class DraftRepository @Inject constructor(private val monarchy: Monarchy) {
} }
) )
return Transformations.map(liveData) { return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList() it.firstOrNull().orEmpty()
} }
} }

View file

@ -126,6 +126,7 @@ internal class RoomDisplayNameResolver @Inject constructor(private val context:
return name ?: roomId return name ?: roomId
} }
/** See [im.vector.matrix.android.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */
private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?, private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?,
roomMemberHelper: RoomMemberHelper): String? { roomMemberHelper: RoomMemberHelper): String? {
if (roomMemberSummary == null) return null if (roomMemberSummary == null) return null

View file

@ -111,7 +111,7 @@ internal class DefaultReadService @AssistedInject constructor(
{ readReceiptsSummaryMapper.map(it) } { readReceiptsSummaryMapper.map(it) }
) )
return Transformations.map(liveRealmData) { return Transformations.map(liveRealmData) {
it.firstOrNull() ?: emptyList() it.firstOrNull().orEmpty()
} }
} }

View file

@ -48,7 +48,7 @@ internal class DefaultUpdateQuickReactionTask @Inject constructor(private val mo
monarchy.doWithRealm { realm -> monarchy.doWithRealm { realm ->
res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId) res = updateQuickReaction(realm, params.reaction, params.oppositeReaction, params.eventId)
} }
return UpdateQuickReactionTask.Result(res?.first, res?.second ?: emptyList()) return UpdateQuickReactionTask.Result(res?.first, res?.second.orEmpty())
} }
private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> { private fun updateQuickReaction(realm: Realm, reaction: String, oppositeReaction: String, eventId: String): Pair<String?, List<String>?> {

View file

@ -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>,

View file

@ -35,6 +35,7 @@ import im.vector.matrix.android.api.session.room.model.message.FileInfo
import im.vector.matrix.android.api.session.room.model.message.ImageInfo import im.vector.matrix.android.api.session.room.model.message.ImageInfo
import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent import im.vector.matrix.android.api.session.room.model.message.MessageAudioContent
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageContentWithFormattedBody
import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageFileContent
import im.vector.matrix.android.api.session.room.model.message.MessageFormat import im.vector.matrix.android.api.session.room.model.message.MessageFormat
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
@ -56,6 +57,7 @@ import im.vector.matrix.android.api.session.room.model.relation.ReplyToContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.extensions.subStringBetween
import im.vector.matrix.android.internal.session.content.ThumbnailExtractor import im.vector.matrix.android.internal.session.content.ThumbnailExtractor
import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils import im.vector.matrix.android.internal.session.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) { ) {
// TODO Inject // TODO Inject
private val parser = Parser.builder().build() private val parser = Parser.builder().build()
// TODO Inject // TODO Inject
private val renderer = HtmlRenderer.builder().build() private val renderer = HtmlRenderer.builder().build()
@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor(
val document = parser.parse(source) val document = parser.parse(source)
val htmlText = renderer.render(document) val htmlText = renderer.render(document)
if (isFormattedTextPertinent(source, htmlText)) { // Cleanup extra paragraph
return TextContent(text.toString(), htmlText) val cleanHtmlText = if (htmlText.startsWith("<p>") && htmlText.endsWith("</p>\n")) {
htmlText.subStringBetween("<p>", "</p>\n")
} else {
htmlText
}
if (isFormattedTextPertinent(source, cleanHtmlText)) {
return TextContent(text.toString(), cleanHtmlText)
} }
} else { } else {
// Try to detect pills // Try to detect pills
@ -192,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor(
permalink, permalink,
stringProvider.getString(R.string.message_reply_to_prefix), stringProvider.getString(R.string.message_reply_to_prefix),
userLink, userLink,
originalEvent.getDisambiguatedDisplayName(), originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(), body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted() createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
) )
@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_TEXT, MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> { MessageType.MSGTYPE_NOTICE -> {
var formattedText: String? = null var formattedText: String? = null
if (content is MessageTextContent) { if (content is MessageContentWithFormattedBody) {
if (content.format == MessageFormat.FORMAT_MATRIX_HTML) { formattedText = content.matrixFormattedBody
formattedText = content.formattedBody
}
} }
val isReply = content.isReply() || originalContent.isReply() val isReply = content.isReply() || originalContent.isReply()
return if (isReply) { return if (isReply) {

View file

@ -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) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) {
return // 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 params = PaginationTask.Params(roomId = roomId, val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId
from = token, if (lastKnownEventId == null) {
direction = direction.toPaginationDirection(), updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
limit = limit) } else {
val params = FetchNextTokenAndPaginateTask.Params(
Timber.v("Should fetch $limit items $direction") roomId = roomId,
cancelableBag += paginationTask limit = limit,
.configureWith(params) { lastKnownEventId = lastKnownEventId
this.callback = object : MatrixCallback<TokenChunkEventPersistor.Result> { )
override fun onSuccess(data: TokenChunkEventPersistor.Result) { cancelableBag += fetchNextTokenAndPaginateTask
when (data) { .configureWith(params) {
TokenChunkEventPersistor.Result.SUCCESS -> { this.callback = createPaginationCallback(limit, direction)
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)
}
} }
} .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)
}
}
override fun onFailure(failure: Throwable) { // For debug purpose only
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } private fun dumpAndLogChunks() {
postSnapshot() val liveChunk = getLiveChunk()
Timber.v("Failure fetching $limit items $direction from pagination request") 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}")
} }
} }
} }
.executeBy(taskExecutor)
} }
/** /**
* 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 {

View file

@ -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
) )
} }

View file

@ -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)
}
}

View file

@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
val end: String? val end: String?
val events: List<Event> val events: List<Event>
val stateEvents: List<Event> val stateEvents: List<Event>
fun hasMore() = start != end
} }

View file

@ -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
@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
} }
?: ChunkEntity.create(realm, prevToken, nextToken) ?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) { if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk) handleReachEnd(realm, roomId, direction, currentChunk)
} else { } else {
handlePagination(realm, roomId, direction, receivedChunk, currentChunk) handlePagination(realm, roomId, direction, receivedChunk, currentChunk)
@ -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
} }
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk) if (currentChunk.isValid) {
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
}
} }
} }

View file

@ -0,0 +1,46 @@
/*
* 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.uploads
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
import im.vector.matrix.android.api.session.room.uploads.UploadsService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith
internal class DefaultUploadsService @AssistedInject constructor(
@Assisted private val roomId: String,
private val taskExecutor: TaskExecutor,
private val getUploadsTask: GetUploadsTask
) : UploadsService {
@AssistedInject.Factory
interface Factory {
fun create(roomId: String): UploadsService
}
override fun getUploads(numberOfEvents: Int, since: String?, callback: MatrixCallback<GetUploadsResult>): Cancelable {
return getUploadsTask
.configureWith(GetUploadsTask.Params(roomId, numberOfEvents, since)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
}

View file

@ -0,0 +1,100 @@
/*
* 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.uploads
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageWithAttachmentContent
import im.vector.matrix.android.api.session.room.sender.SenderInfo
import im.vector.matrix.android.api.session.room.uploads.GetUploadsResult
import im.vector.matrix.android.api.session.room.uploads.UploadEvent
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.filter.FilterFactory
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.membership.RoomMemberHelper
import im.vector.matrix.android.internal.session.room.timeline.PaginationDirection
import im.vector.matrix.android.internal.session.room.timeline.PaginationResponse
import im.vector.matrix.android.internal.session.sync.SyncTokenStore
import im.vector.matrix.android.internal.task.Task
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject
internal interface GetUploadsTask : Task<GetUploadsTask.Params, GetUploadsResult> {
data class Params(
val roomId: String,
val numberOfEvents: Int,
val since: String?
)
}
internal class DefaultGetUploadsTask @Inject constructor(
private val roomAPI: RoomAPI,
private val tokenStore: SyncTokenStore,
private val monarchy: Monarchy,
private val eventBus: EventBus)
: GetUploadsTask {
override suspend fun execute(params: GetUploadsTask.Params): GetUploadsResult {
val since = params.since ?: tokenStore.getLastToken() ?: throw IllegalStateException("No token available")
val filter = FilterFactory.createUploadsFilter(params.numberOfEvents).toJSONString()
val chunk = executeRequest<PaginationResponse>(eventBus) {
apiCall = roomAPI.getRoomMessagesFrom(params.roomId, since, PaginationDirection.BACKWARDS.value, params.numberOfEvents, filter)
}
var uploadEvents = listOf<UploadEvent>()
val cacheOfSenderInfos = mutableMapOf<String, SenderInfo>()
// Get a snapshot of all room members
monarchy.doWithRealm { realm ->
val roomMemberHelper = RoomMemberHelper(realm, params.roomId)
uploadEvents = chunk.events.mapNotNull { event ->
val eventId = event.eventId ?: return@mapNotNull null
val messageContent = event.getClearContent()?.toModel<MessageContent>() ?: return@mapNotNull null
val messageWithAttachmentContent = (messageContent as? MessageWithAttachmentContent) ?: return@mapNotNull null
val senderId = event.senderId ?: return@mapNotNull null
val senderInfo = cacheOfSenderInfos.getOrPut(senderId) {
val roomMemberSummaryEntity = roomMemberHelper.getLastRoomMember(senderId)
SenderInfo(
userId = senderId,
displayName = roomMemberSummaryEntity?.displayName,
isUniqueDisplayName = roomMemberHelper.isUniqueDisplayName(roomMemberSummaryEntity?.displayName),
avatarUrl = roomMemberSummaryEntity?.avatarUrl
)
}
UploadEvent(
root = event,
eventId = eventId,
contentWithAttachmentContent = messageWithAttachmentContent,
senderInfo = senderInfo
)
}
}
return GetUploadsResult(
uploadEvents = uploadEvents,
nextToken = chunk.end ?: "",
hasMore = chunk.hasMore()
)
}
}

View file

@ -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

View file

@ -44,7 +44,7 @@ data class TermsResponse(
version = tos[VERSION] as? String version = tos[VERSION] as? String
) )
} }
}?.filterNotNull() ?: emptyList() }?.filterNotNull().orEmpty()
} }
private companion object { private companion object {

View file

@ -41,5 +41,5 @@ internal abstract class AccountDataModule {
abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
@Binds @Binds
abstract fun bindUpdateBreadcrumsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask abstract fun bindUpdateBreadcrumbsTask(task: DefaultUpdateBreadcrumbsTask): UpdateBreadcrumbsTask
} }

View file

@ -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>

View file

@ -260,6 +260,7 @@ dependencies {
def autofill_version = "1.0.0" def autofill_version = "1.0.0"
def work_version = '2.3.3' def work_version = '2.3.3'
def arch_version = '2.1.0' def arch_version = '2.1.0'
def lifecycle_version = '2.2.0'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
@ -282,6 +283,7 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0" implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
implementation "com.squareup.moshi:moshi-adapters:$moshi_version" implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version" kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
// Log // Log

View file

@ -45,11 +45,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context,
if (time == null) { if (time == null) {
return "" return ""
} }
return DateUtils.getRelativeDateTimeString(context, return DateUtils.getRelativeDateTimeString(
time, context,
DateUtils.DAY_IN_MILLIS, time,
2 * DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY 2 * DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
).toString() ).toString()
} }
} }

View file

@ -76,6 +76,9 @@ import im.vector.riotx.features.roommemberprofile.devices.DeviceTrustInfoActionF
import im.vector.riotx.features.roomprofile.RoomProfileFragment import im.vector.riotx.features.roomprofile.RoomProfileFragment
import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment import im.vector.riotx.features.roomprofile.members.RoomMemberListFragment
import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment import im.vector.riotx.features.roomprofile.settings.RoomSettingsFragment
import im.vector.riotx.features.roomprofile.uploads.RoomUploadsFragment
import im.vector.riotx.features.roomprofile.uploads.files.RoomUploadsFilesFragment
import im.vector.riotx.features.roomprofile.uploads.media.RoomUploadsMediaFragment
import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment import im.vector.riotx.features.settings.VectorSettingsAdvancedNotificationPreferenceFragment
import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment import im.vector.riotx.features.settings.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment import im.vector.riotx.features.settings.VectorSettingsLabsFragment
@ -310,6 +313,21 @@ interface FragmentModule {
@FragmentKey(RoomMemberListFragment::class) @FragmentKey(RoomMemberListFragment::class)
fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment fun bindRoomMemberListFragment(fragment: RoomMemberListFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsFragment::class)
fun bindRoomUploadsFragment(fragment: RoomUploadsFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsMediaFragment::class)
fun bindRoomUploadsMediaFragment(fragment: RoomUploadsMediaFragment): Fragment
@Binds
@IntoMap
@FragmentKey(RoomUploadsFilesFragment::class)
fun bindRoomUploadsFilesFragment(fragment: RoomUploadsFilesFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(RoomSettingsFragment::class) @FragmentKey(RoomSettingsFragment::class)

View file

@ -75,8 +75,8 @@ abstract class VectorModule {
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds @Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter
@Binds @Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository abstract fun bindUiStateRepository(repository: SharedPreferencesUiStateRepository): UiStateRepository
} }

View file

@ -0,0 +1,26 @@
/*
* 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.epoxy
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
@EpoxyModelClass(layout = R.layout.item_loading_square)
abstract class SquareLoadingItem : VectorEpoxyModel<SquareLoadingItem.Holder>() {
class Holder : VectorEpoxyHolder()
}

View file

@ -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)

View file

@ -80,5 +80,12 @@ fun <T : Fragment> VectorBaseFragment.addChildFragmentToBackstack(frameId: Int,
} }
} }
/**
* Return a list of all child Fragments, recursively
*/
fun Fragment.getAllChildFragments(): List<Fragment> {
return listOf(this) + childFragmentManager.fragments.map { it.getAllChildFragments() }.flatten()
}
// Define a missing constant // Define a missing constant
const val POP_BACK_STACK_EXCLUSIVE = 0 const val POP_BACK_STACK_EXCLUSIVE = 0

View file

@ -21,6 +21,7 @@ import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyVisibilityTracker
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
fun RecyclerView.cleanup() { fun RecyclerView.cleanup() {
adapter = null adapter = null
} }
fun RecyclerView.trackItemsVisibilityChange() = EpoxyVisibilityTracker().attach(this)

View file

@ -21,6 +21,7 @@ import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.view.isVisible
import im.vector.riotx.R import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_state.view.* import kotlinx.android.synthetic.main.view_state.view.*
@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
object Content : State() object Content : State()
object Loading : State() object Loading : State()
data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State() data class Empty(val title: CharSequence? = null, val image: Drawable? = null, val message: CharSequence? = null) : State()
data class Error(val message: CharSequence? = null) : State() data class Error(val message: CharSequence? = null) : State()
} }
@ -59,34 +61,21 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
} }
private fun update(newState: State) { private fun update(newState: State) {
progressBar.isVisible = newState is State.Loading
errorView.isVisible = newState is State.Error
emptyView.isVisible = newState is State.Empty
contentView?.isVisible = newState is State.Content
when (newState) { when (newState) {
is State.Content -> { is State.Content -> Unit
progressBar.visibility = View.INVISIBLE is State.Loading -> Unit
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.VISIBLE
}
is State.Loading -> {
progressBar.visibility = View.VISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.INVISIBLE
contentView?.visibility = View.INVISIBLE
}
is State.Empty -> { is State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image) emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message emptyMessageView.text = newState.message
emptyTitleView.text = newState.title emptyTitleView.text = newState.title
contentView?.visibility = View.INVISIBLE
} }
is State.Error -> { is State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message errorMessageView.text = newState.message
contentView?.visibility = View.INVISIBLE
} }
} }
} }

View file

@ -16,5 +16,7 @@
package im.vector.riotx.core.ui.model package im.vector.riotx.core.ui.model
import androidx.annotation.Px
// android.util.Size in API 21+ // android.util.Size in API 21+
data class Size(val width: Int, val height: Int) data class Size(@Px val width: Int, @Px val height: Int)

View file

@ -17,10 +17,12 @@ package im.vector.riotx.core.utils
import android.content.res.Resources import android.content.res.Resources
import android.util.TypedValue import android.util.TypedValue
import androidx.annotation.Px
import javax.inject.Inject import javax.inject.Inject
class DimensionConverter @Inject constructor(val resources: Resources) { class DimensionConverter @Inject constructor(val resources: Resources) {
@Px
fun dpToPx(dp: Int): Int { fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, TypedValue.COMPLEX_UNIT_DIP,
@ -29,6 +31,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
).toInt() ).toInt()
} }
@Px
fun spToPx(sp: Int): Int { fun spToPx(sp: Int): Int {
return TypedValue.applyDimension( return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP, TypedValue.COMPLEX_UNIT_SP,
@ -36,4 +39,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
resources.displayMetrics resources.displayMetrics
).toInt() ).toInt()
} }
fun pxToDp(@Px px: Int): Int {
return (px.toFloat() / resources.displayMetrics.density).toInt()
}
} }

View file

@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
sendIntent.type = mediaMimeType sendIntent.type = mediaMimeType
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri) sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri)
context.startActivity(sendIntent) try {
context.startActivity(sendIntent)
} catch (activityNotFoundException: ActivityNotFoundException) {
context.toast(R.string.error_no_external_application_found)
}
} }
} }

View file

@ -43,7 +43,7 @@ class AttachmentsPreviewActivity : VectorBaseActivity(), ToolbarConfigurable {
} }
fun getOutput(intent: Intent): List<ContentAttachmentData> { fun getOutput(intent: Intent): List<ContentAttachmentData> {
return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT) ?: emptyList() return intent.getParcelableArrayListExtra<ContentAttachmentData>(ATTACHMENTS_PREVIEW_RESULT).orEmpty()
} }
fun getKeepOriginalSize(intent: Intent): Boolean { fun getKeepOriginalSize(intent: Intent): Boolean {

View file

@ -151,8 +151,8 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
private fun changeThreePidState(threePid: ThreePid, state: Async<SharedState>) { private fun changeThreePidState(threePid: ThreePid, state: Async<SharedState>) {
setState { setState {
val currentMails = emailList() ?: emptyList() val currentMails = emailList().orEmpty()
val phones = phoneNumbersList() ?: emptyList() val phones = phoneNumbersList().orEmpty()
copy( copy(
emailList = Success( emailList = Success(
currentMails.map { currentMails.map {
@ -178,8 +178,8 @@ class DiscoverySettingsViewModel @AssistedInject constructor(
private fun changeThreePidSubmitState(threePid: ThreePid, submitState: Async<Unit>) { private fun changeThreePidSubmitState(threePid: ThreePid, submitState: Async<Unit>) {
setState { setState {
val currentMails = emailList() ?: emptyList() val currentMails = emailList().orEmpty()
val phones = phoneNumbersList() ?: emptyList() val phones = phoneNumbersList().orEmpty()
copy( copy(
emailList = Success( emailList = Success(
currentMails.map { currentMails.map {

View file

@ -123,12 +123,12 @@ class HomeDetailFragment @Inject constructor(
?.navigator ?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "") ?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
dismissedAction = Runnable { dismissedAction = Runnable {
unknownDeviceDetectorSharedViewModel.handle( unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList()) UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
) )
} }
} }

View file

@ -22,7 +22,6 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.text.Spannable import android.text.Spannable
@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.Window
import android.widget.Toast import android.widget.Toast
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
@ -49,7 +46,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
@ -95,6 +91,7 @@ import im.vector.riotx.core.extensions.exhaustive
import im.vector.riotx.core.extensions.hideKeyboard import im.vector.riotx.core.extensions.hideKeyboard
import im.vector.riotx.core.extensions.setTextOrHide import im.vector.riotx.core.extensions.setTextOrHide
import im.vector.riotx.core.extensions.showKeyboard import im.vector.riotx.core.extensions.showKeyboard
import im.vector.riotx.core.extensions.trackItemsVisibilityChange
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.intent.getMimeTypeFromUri import im.vector.riotx.core.intent.getMimeTypeFromUri
@ -150,9 +147,7 @@ import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.PillImageSpan import im.vector.riotx.features.html.PillImageSpan
import im.vector.riotx.features.invite.VectorInviteView import im.vector.riotx.features.invite.VectorInviteView
import im.vector.riotx.features.media.ImageContentRenderer import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoContentRenderer import im.vector.riotx.features.media.VideoContentRenderer
import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.permalink.NavigationInterceptor import im.vector.riotx.features.permalink.NavigationInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler import im.vector.riotx.features.permalink.PermalinkHandler
@ -469,7 +464,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleter.enterSpecialMode() autoCompleter.enterSpecialMode()
// switch to expanded bar // switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply { composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName() text = event.senderInfo.disambiguatedDisplayName
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId))) setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
} }
@ -488,11 +483,7 @@ class RoomDetailFragment @Inject constructor(
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes)) composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.contentDescription = getString(descriptionRes) composerLayout.sendButton.contentDescription = getString(descriptionRes)
avatarRenderer.render( avatarRenderer.render(event.senderInfo.toMatrixItem(), composerLayout.composerRelatedMessageAvatar)
MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
composerLayout.expand { composerLayout.expand {
if (isAdded) { if (isAdded) {
@ -549,8 +540,7 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.callback = this timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline timelineEventController.timeline = roomDetailViewModel.timeline
val epoxyVisibilityTracker = EpoxyVisibilityTracker() recyclerView.trackItemsVisibilityChange()
epoxyVisibilityTracker.attach(recyclerView)
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register() val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
@ -1004,31 +994,14 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) { override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view)) pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
val pairs = ArrayList<Pair<View, String>>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
requireActivity().window.decorView.findViewById<View>(android.R.id.statusBarBackground)?.let {
pairs.add(Pair(it, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME))
}
requireActivity().window.decorView.findViewById<View>(android.R.id.navigationBarBackground)?.let {
pairs.add(Pair(it, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME))
}
} }
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
requireActivity(), *pairs.toTypedArray()).toBundle()
startActivity(intent, bundle)
} }
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) { override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
// TODO Use navigator navigator.openVideoViewer(requireActivity(), mediaData)
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
} }
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) { override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

Some files were not shown because too many files have changed in this diff Show more