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 ✨:
- Identity server support (#607)
- Switch language support (#41)
- Display list of attachments of a room (#860)
Improvements 🙌:
- Better connectivity lost indicator when airplane mode is on
- 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 🐛:
- After jump to unread, newer messages are never loaded (#1008)
- 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 🗣:
-
SDK API changes ⚠️:
-
- initialize with proxy configuration
Build 🧱:
-
Other changes:
-
- support new key agreement method for SAS (#1374)
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.session.Session
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.room.Room
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.TimelineEvent
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> {
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
val latch = CountDownLatch(nbOfMessages)
val latch = CountDownLatch(1)
val timelineListener = object : Timeline.Listener {
override fun onTimelineFailure(throwable: Throwable) {
}
@ -128,7 +128,7 @@ class CommonTestHelper(context: Context) {
override fun onTimelineUpdated(snapshot: List<TimelineEvent>) {
val newMessages = snapshot
.filter { LocalEcho.isLocalEchoId(it.eventId).not() }
.filter { it.root.sendState == SendState.SYNCED }
.filter { it.root.getClearType() == EventType.MESSAGE }
.filter { it.root.getClearContent().toModel<MessageContent>()?.body?.startsWith(message) == true }
@ -144,7 +144,8 @@ class CommonTestHelper(context: Context) {
for (i in 0 until nbOfMessages) {
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.dispose()
@ -292,6 +293,24 @@ class CommonTestHelper(context: Context) {
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
*
@ -350,3 +369,13 @@ class CommonTestHelper(context: Context) {
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,18 +53,20 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
/**
* @return alice session
*/
fun doE2ETestWithAliceInARoom(): CryptoTestData {
fun doE2ETestWithAliceInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams)
val roomId = mTestHelper.doSync<String> {
aliceSession.createRoom(CreateRoomParams(name = "MyRoom"), it)
}
if (encryptedRoom) {
val room = aliceSession.getRoom(roomId)!!
mTestHelper.doSync<Unit> {
room.enableEncryption(callback = it)
}
}
return CryptoTestData(aliceSession, roomId)
}
@ -72,8 +74,8 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) {
/**
* @return alice and bob sessions
*/
fun doE2ETestWithAliceAndBobInARoom(): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom()
fun doE2ETestWithAliceAndBobInARoom(encryptedRoom: Boolean = true): CryptoTestData {
val cryptoTestData = doE2ETestWithAliceInARoom(encryptedRoom)
val aliceSession = cryptoTestData.firstSession
val aliceRoomId = cryptoTestData.roomId

View File

@ -468,15 +468,20 @@ class SASTest : InstrumentedTest {
val aliceSASLatch = CountDownLatch(1)
val aliceListener = object : VerificationService.Listener {
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as OutgoingSasVerificationTransaction).uxState
Log.v("TEST", "== aliceState ${uxState.name}")
when (uxState) {
OutgoingSasVerificationTransaction.UxState.SHOW_SAS -> {
tx.userHasVerifiedShortCode()
}
OutgoingSasVerificationTransaction.UxState.VERIFIED -> {
if (matchOnce) {
matchOnce = false
aliceSASLatch.countDown()
}
}
else -> Unit
}
}
@ -485,15 +490,24 @@ class SASTest : InstrumentedTest {
val bobSASLatch = CountDownLatch(1)
val bobListener = object : VerificationService.Listener {
var acceptOnce = true
var matchOnce = true
override fun transactionUpdated(tx: VerificationTransaction) {
val uxState = (tx as IncomingSasVerificationTransaction).uxState
Log.v("TEST", "== bobState ${uxState.name}")
when (uxState) {
IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT -> {
if (acceptOnce) {
acceptOnce = false
tx.performAccept()
}
}
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
if (matchOnce) {
matchOnce = false
tx.userHasVerifiedShortCode()
}
}
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
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 im.vector.matrix.android.BuildConfig
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.crypto.attachments.ElementToDecrypt
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 org.matrix.olm.OlmManager
import java.io.InputStream
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicBoolean
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.
* 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.
*/
data class MXCryptoConfig(
data class MXCryptoConfig constructor(
// Tell whether the encryption of the event content is enabled for the invited members.
// 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".
var enableEncryptionForInvitedMembers: Boolean = true,
val enableEncryptionForInvitedMembers: Boolean = true,
/**
* 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
* 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
}
}
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.timeline.TimelineService
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
/**
@ -42,6 +43,7 @@ interface Room :
TypingService,
MembershipService,
StateService,
UploadsService,
ReportingService,
RelationService,
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,
/**
* 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.
*/
@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.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,
/**
* 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.
*/
@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.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,
/**
* 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.
*/
@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.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.
* @param the direction to check in
* @param direction the direction to check in
* @return true if timeline can be enriched
*/
fun hasMoreToLoad(direction: Direction): Boolean

View File

@ -16,6 +16,7 @@
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.EventType
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.MessageStickerContent
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.internal.crypto.model.event.EncryptedEventContent
@ -38,13 +40,17 @@ data class TimelineEvent(
val localId: Long,
val eventId: String,
val displayIndex: Int,
val senderName: String?,
val isUniqueDisplayName: Boolean,
val senderAvatar: String?,
val senderInfo: SenderInfo,
val annotations: EventAnnotationsSummary? = null,
val readReceipts: List<ReadReceipt> = emptyList()
) {
init {
if (BuildConfig.DEBUG) {
assert(eventId == root.eventId)
}
}
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.
* @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.RoomSummary
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 java.util.Locale
@ -154,3 +155,5 @@ fun RoomSummary.toRoomAliasMatrixItem() = MatrixItem.RoomAliasItem(canonicalAlia
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name ?: getPrimaryAlias() ?: "", 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 } }
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) {
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<*, *>))
}
if (type in completedStages ?: emptyList()) {
if (type in completedStages.orEmpty()) {
completedStage.add(stage)
} else {
missingStage.add(stage)

View File

@ -262,7 +262,7 @@ internal class DefaultCryptoService @Inject constructor(
override fun onSuccess(data: DevicesListResponse) {
// Save in local DB
cryptoStore.saveMyDevicesInfo(data.devices ?: emptyList())
cryptoStore.saveMyDevicesInfo(data.devices.orEmpty())
callback.onSuccess(data)
}
}
@ -446,7 +446,7 @@ internal class DefaultCryptoService @Inject constructor(
}
override fun getCryptoDeviceInfo(userId: String): List<CryptoDeviceInfo> {
return cryptoStore.getUserDeviceList(userId) ?: emptyList()
return cryptoStore.getUserDeviceList(userId).orEmpty()
}
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 {
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) ->
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> {
Timber.v("## ensureOlmSessionsForUsers() : ensureOlmSessionsForUsers $users")
val devicesByUser = users.associateWith { userId ->
val devices = cryptoStore.getUserDevices(userId)?.values ?: emptyList()
val devices = cryptoStore.getUserDevices(userId)?.values.orEmpty()
devices.filter {
// Don't bother setting up session to ourself

View File

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

View File

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

View File

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

View File

@ -450,7 +450,7 @@ internal class RealmCryptoStore @Inject constructor(
}
)
return Transformations.map(liveData) {
it.firstOrNull() ?: emptyList()
it.firstOrNull().orEmpty()
}
}
@ -480,7 +480,7 @@ internal class RealmCryptoStore @Inject constructor(
}
)
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) {
Timber.d("Step 2 -> 3")
Timber.d("Updating CryptoMetadataEntity table")
realm.schema.get("CryptoMetadataEntity")
?.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) {
Timber.d("Step 3 -> 4")
Timber.d("Updating KeyInfoEntity table")
val keyInfoEntities = realm.where("KeyInfoEntity").findAll()
try {
@ -238,6 +240,7 @@ internal class RealmCryptoStoreMigration @Inject constructor(private val crossSi
}
private fun migrateTo5(realm: DynamicRealm) {
Timber.d("Step 4 -> 5")
realm.schema.create("MyDeviceLastSeenInfoEntity")
.addField(MyDeviceLastSeenInfoEntityFields.DEVICE_ID, String::class.java)
.addPrimaryKey(MyDeviceLastSeenInfoEntityFields.DEVICE_ID)

View File

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

View File

@ -198,6 +198,20 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// using the result as the shared secret.
getSAS().setTheirPublicKey(otherKey)
shortCodeBytes = calculateSASBytes()
if (BuildConfig.LOG_PRIVATE_DATA) {
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
}
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”,
@ -207,16 +221,21 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
// - 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) {
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
Timber.v("************ BOB EMOJI CODE ${getShortCodeRepresentation(SasMode.EMOJI)}")
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()
}
}
state = VerificationTxState.ShortCodeReady
}
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {

View File

@ -193,6 +193,17 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
if (accepted!!.commitment.equals(otherCommitment)) {
getSAS().setTheirPublicKey(otherKey)
shortCodeBytes = calculateSASBytes()
state = VerificationTxState.ShortCodeReady
} else {
// bad commitment
cancel(CancelCode.MismatchedCommitment)
}
}
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”,
@ -202,24 +213,33 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
// - 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
} else {
// bad commitment
cancel(CancelCode.MismatchedCommitment)
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) {
Timber.v("## SAS O: onKeyVerificationMac id:$transactionId")
// There is starting to be a huge amount of state / race here :/
if (state != VerificationTxState.OnKeyReceived
&& state != VerificationTxState.ShortCodeReady
&& state != VerificationTxState.ShortCodeAccepted
&& state != VerificationTxState.KeySent
&& state != VerificationTxState.SendingMac
&& 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)
return
}

View File

@ -611,7 +611,7 @@ internal class DefaultVerificationService @Inject constructor(
if (validCancelReq == null) {
// ignore
Timber.e("## SAS Received invalid key request")
Timber.e("## SAS Received invalid cancel request")
// TODO should we cancel?
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.OutgoingGossipingRequestManager
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.extensions.toUnsignedInt
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 = "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
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
val KNOWN_HASHES = listOf("sha256")
// ordered by preferred order

View File

@ -60,8 +60,7 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct
chunkToMerge.stateEvents.forEach { stateEvent ->
addStateEvent(roomId, stateEvent, direction)
}
return eventsToMerge
.forEach {
eventsToMerge.forEach {
addTimelineEventFromMerge(localRealm, it, direction)
}
}

View File

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

View File

@ -49,7 +49,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) }.orEmpty(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList(),
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.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.internal.database.model.TimelineEventEntity
import javax.inject.Inject
@ -41,15 +41,18 @@ internal class TimelineEventMapper @Inject constructor(private val readReceiptsS
annotations = timelineEventEntity.annotations?.asDomain(),
localId = timelineEventEntity.localId,
displayIndex = timelineEventEntity.displayIndex,
senderName = timelineEventEntity.senderName,
senderInfo = SenderInfo(
userId = timelineEventEntity.root?.sender ?: "",
displayName = timelineEventEntity.senderName,
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
senderAvatar = timelineEventEntity.senderAvatar,
avatarUrl = timelineEventEntity.senderAvatar
),
readReceipts = readReceipts
?.distinctBy {
it.user
}?.sortedByDescending {
it.originServerTs
} ?: emptyList()
}.orEmpty()
)
}
}

View File

@ -23,15 +23,20 @@ import io.realm.annotations.Index
import io.realm.annotations.LinkingObjects
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,
var stateEvents: RealmList<EventEntity> = RealmList(),
var timelineEvents: RealmList<TimelineEventEntity> = RealmList(),
// Only one chunk will have isLastForward == true
@Index var isLastForward: Boolean = false,
@Index var isLastBackward: Boolean = false
) : RealmObject() {
fun identifier() = "${prevToken}_$nextToken"
// If true, then this chunk was previously a last forward chunk
fun hasBeenALastForwardChunk() = nextToken == null && !isLastForward
@LinkingObjects("chunks")
val room: RealmResults<RoomEntity>? = null

View File

@ -41,7 +41,7 @@ internal fun ChunkEntity.Companion.find(realm: Realm, roomId: String, prevToken:
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)
.equalTo(ChunkEntityFields.IS_LAST_FORWARD, true)
.findFirst()

View File

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

View File

@ -36,7 +36,7 @@ internal fun isEventRead(monarchy: Monarchy,
var isEventRead = false
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)
isEventRead = if (eventToCheck == null || eventToCheck.root?.sender == userId) {
true

View File

@ -59,7 +59,7 @@ internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm,
filterTypes: List<String> = emptyList()): TimelineEventEntity? {
val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: return null
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) {
liveEvents
?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)

View File

@ -21,6 +21,7 @@ import com.squareup.moshi.Moshi
import dagger.Module
import dagger.Provides
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.UserAgentInterceptor
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
@ -64,7 +65,8 @@ internal object NetworkModule {
@Provides
@JvmStatic
@Unauthenticated
fun providesOkHttpClient(stethoInterceptor: StethoInterceptor,
fun providesOkHttpClient(matrixConfiguration: MatrixConfiguration,
stethoInterceptor: StethoInterceptor,
timeoutInterceptor: TimeOutInterceptor,
userAgentInterceptor: UserAgentInterceptor,
httpLoggingInterceptor: HttpLoggingInterceptor,
@ -82,6 +84,9 @@ internal object NetworkModule {
if (BuildConfig.LOG_PRIVATE_DATA) {
addInterceptor(curlLoggingInterceptor)
}
matrixConfiguration.proxy?.let {
proxy(it)
}
}
.addInterceptor(okReplayInterceptor)
.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
@Binds
abstract fun bindNetworkConnectivityChecker(networkConnectivityChecker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
abstract fun bindNetworkConnectivityChecker(checker: DefaultNetworkConnectivityChecker): NetworkConnectivityChecker
@Binds
@IntoSet
abstract fun bindGroupSummaryUpdater(groupSummaryUpdater: GroupSummaryUpdater): LiveEntityObserver
abstract fun bindGroupSummaryUpdater(updater: GroupSummaryUpdater): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindEventsPruner(eventsPruner: EventsPruner): LiveEntityObserver
abstract fun bindEventsPruner(pruner: EventsPruner): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
abstract fun bindEventRelationsAggregationUpdater(updater: EventRelationsAggregationUpdater): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindRoomTombstoneEventLiveObserver(roomTombstoneEventLiveObserver: RoomTombstoneEventLiveObserver): LiveEntityObserver
abstract fun bindRoomTombstoneEventLiveObserver(observer: RoomTombstoneEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindRoomCreateEventLiveObserver(roomCreateEventLiveObserver: RoomCreateEventLiveObserver): LiveEntityObserver
abstract fun bindRoomCreateEventLiveObserver(observer: RoomCreateEventLiveObserver): LiveEntityObserver
@Binds
@IntoSet
abstract fun bindVerificationMessageLiveObserver(verificationMessageLiveObserver: VerificationMessageLiveObserver): LiveEntityObserver
abstract fun bindVerificationMessageLiveObserver(observer: VerificationMessageLiveObserver): LiveEntityObserver
@Binds
abstract fun bindInitialSyncProgressService(initialSyncProgressService: DefaultInitialSyncProgressService): InitialSyncProgressService
abstract fun bindInitialSyncProgressService(service: DefaultInitialSyncProgressService): InitialSyncProgressService
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
abstract fun bindSecureStorageService(service: DefaultSecureStorageService): SecureStorageService
@Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
abstract fun bindHomeServerCapabilitiesService(service: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
@Binds
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 {
@Binds
abstract fun bindContentUploadStateTracker(contentUploadStateTracker: DefaultContentUploadStateTracker): ContentUploadStateTracker
abstract fun bindContentUploadStateTracker(tracker: DefaultContentUploadStateTracker): ContentUploadStateTracker
@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 {
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 ->
val filter = FilterEntity.get(realm)
val filterEntity = FilterEntity.get(realm)
// Filter has changed, or no filter Id yet
filter == null
|| filter.filterBodyJson != filterBody.toJSONString()
|| filter.filterId.isBlank()
filterEntity == null
|| filterEntity.filterBodyJson != filter.toJSONString()
|| filterEntity.filterId.isBlank()
}.also { hasChanged ->
if (hasChanged) {
// 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
monarchy.awaitTransaction { realm ->
// We manage only one filter for now
val filterBodyJson = filterBody.toJSONString()
val filterJson = filter.toJSONString()
val roomEventFilterJson = roomEventFilter.toJSONString()
val filterEntity = FilterEntity.getOrCreate(realm)
filterEntity.filterBodyJson = filterBodyJson
filterEntity.filterBodyJson = filterJson
filterEntity.roomEventFilterJson = roomEventFilterJson
// Reset 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 {
// 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
it.where<FilterEntity>()
.equalTo(FilterEntityFields.FILTER_BODY_JSON, filterBodyJson)
.equalTo(FilterEntityFields.FILTER_BODY_JSON, filterJson)
?.findFirst()
?.filterId = filterId
}

View File

@ -43,10 +43,10 @@ internal class DefaultSaveFilterTask @Inject constructor(
override suspend fun execute(params: SaveFilterTask.Params) {
val filterBody = when (params.filterPreset) {
FilterService.FilterPreset.RiotFilter -> {
FilterFactory.createRiotFilterBody()
FilterFactory.createRiotFilter()
}
FilterService.FilterPreset.NoFilter -> {
FilterFactory.createDefaultFilterBody()
FilterFactory.createDefaultFilter()
}
}
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.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
*/
@JsonClass(generateAdapter = true)
data class Filter(
@Json(name = "limit") val limit: Int? = null,
@Json(name = "senders") val senders: List<String>? = null,
@Json(name = "not_senders") val notSenders: List<String>? = null,
@Json(name = "types") val types: List<String>? = null,
@Json(name = "not_types") val notTypes: List<String>? = null,
@Json(name = "rooms") val rooms: List<String>? = null,
@Json(name = "not_rooms") val notRooms: List<String>? = null
internal data class Filter(
/**
* List of event fields to include. If this list is absent then all fields are included. The entries may
* include '.' characters to indicate sub-fields. So ['content.body'] will include the 'body' field of the
* 'content' object. A literal '.' character in a field name may be escaped using a '\'. A server may
* include more fields than were requested.
*/
@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
|| senders != null
|| notSenders != null
|| types != null
|| notTypes != null
|| rooms != null
|| notRooms != null)
fun toJSONString(): String {
return MoshiProvider.providesMoshi().adapter(Filter::class.java).toJson(this)
}
}

View File

@ -32,7 +32,7 @@ internal interface FilterApi {
* @param body the Json representation of a FilterBody object
*/
@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
@ -42,6 +42,5 @@ internal interface FilterApi {
* @return Filter
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/filter/{filterId}")
fun getFilterById(@Path("userId") userId: String, @Path("filterId")
filterId: String): Call<FilterBody>
fun getFilterById(@Path("userId") userId: String, @Path("filterId") filterId: String): Call<Filter>
}

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 {
fun createDefaultFilterBody(): FilterBody {
return FilterUtil.enableLazyLoading(FilterBody(), true)
fun createUploadsFilter(numberOfEvents: Int): RoomEventFilter {
return RoomEventFilter(
limit = numberOfEvents,
containsUrl = true,
types = listOf(EventType.MESSAGE),
lazyLoadMembers = true
)
}
fun createRiotFilterBody(): FilterBody {
return FilterBody(
fun createDefaultFilter(): Filter {
return FilterUtil.enableLazyLoading(Filter(), true)
}
fun createRiotFilter(): Filter {
return Filter(
room = RoomFilter(
timeline = createRiotTimelineFilter(),
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
*/
suspend fun storeFilter(filterBody: FilterBody, roomEventFilter: RoomEventFilter): Boolean
suspend fun storeFilter(filter: Filter, roomEventFilter: RoomEventFilter): Boolean
/**
* 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

View File

@ -24,5 +24,10 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
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
)

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})}
*
* @param filterBody filterBody to patch
* @param filter filter to patch
* @param useLazyLoading true to enable lazy loading
*/
fun enableLazyLoading(filterBody: FilterBody, useLazyLoading: Boolean): FilterBody {
fun enableLazyLoading(filter: Filter, useLazyLoading: Boolean): Filter {
if (useLazyLoading) {
// Enable lazy loading
return filterBody.copy(
room = filterBody.room?.copy(
state = filterBody.room.state?.copy(lazyLoadMembers = true)
return filter.copy(
room = filter.room?.copy(
state = filter.room.state?.copy(lazyLoadMembers = true)
?: RoomEventFilter(lazyLoadMembers = true)
)
?: RoomFilter(state = RoomEventFilter(lazyLoadMembers = true))
)
} else {
val newRoomEventFilter = filterBody.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() }
val newRoomFilter = filterBody.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() }
val newRoomEventFilter = filter.room?.state?.copy(lazyLoadMembers = null)?.takeIf { it.hasData() }
val newRoomFilter = filter.room?.copy(state = newRoomEventFilter)?.takeIf { it.hasData() }
return filterBody.copy(
return filter.copy(
room = newRoomFilter
)
}

View File

@ -25,14 +25,46 @@ import im.vector.matrix.android.internal.di.MoshiProvider
*/
@JsonClass(generateAdapter = true)
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,
/**
* 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,
/**
* 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 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 room IDs to include. If this list is absent then all rooms are included.
*/
@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,
/**
* 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,
/**
* 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
) {

View File

@ -24,12 +24,37 @@ import com.squareup.moshi.JsonClass
*/
@JsonClass(generateAdapter = true)
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,
/**
* 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,
/**
* 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,
/**
* Include rooms that the user has left in the sync, default false
*/
@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,
/**
* The message and state update events to include for rooms.
*/
@Json(name = "timeline") val timeline: RoomEventFilter? = null,
/**
* The per user account data to include for rooms.
*/
@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 {
return UserAccountDataEvent(
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.timeline.TimelineService
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.toOptional
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 draftService: DraftService,
private val stateService: StateService,
private val uploadsService: UploadsService,
private val reportingService: ReportingService,
private val readService: ReadService,
private val typingService: TypingService,
@ -68,6 +70,7 @@ internal class DefaultRoom @Inject constructor(override val roomId: String,
SendService by sendService,
DraftService by draftService,
StateService by stateService,
UploadsService by uploadsService,
ReportingService by reportingService,
ReadService by readService,
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.timeline.DefaultTimelineService
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 javax.inject.Inject
@ -47,6 +48,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
private val sendServiceFactory: DefaultSendService.Factory,
private val draftServiceFactory: DefaultDraftService.Factory,
private val stateServiceFactory: DefaultStateService.Factory,
private val uploadsServiceFactory: DefaultUploadsService.Factory,
private val reportingServiceFactory: DefaultReportingService.Factory,
private val readServiceFactory: DefaultReadService.Factory,
private val typingServiceFactory: DefaultTypingService.Factory,
@ -66,6 +68,7 @@ internal class DefaultRoomFactory @Inject constructor(private val monarchy: Mona
sendService = sendServiceFactory.create(roomId),
draftService = draftServiceFactory.create(roomId),
stateService = stateServiceFactory.create(roomId),
uploadsService = uploadsServiceFactory.create(roomId),
reportingService = reportingServiceFactory.create(roomId),
readService = readServiceFactory.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.state.DefaultSendStateTask
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.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.PaginationTask
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.uploads.DefaultGetUploadsTask
import im.vector.matrix.android.internal.session.room.uploads.GetUploadsTask
import retrofit2.Retrofit
@Module
@ -143,6 +147,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindPaginationTask(task: DefaultPaginationTask): PaginationTask
@Binds
abstract fun bindFetchNextTokenAndPaginateTask(task: DefaultFetchNextTokenAndPaginateTask): FetchNextTokenAndPaginateTask
@Binds
abstract fun bindFetchEditHistoryTask(task: DefaultFetchEditHistoryTask): FetchEditHistoryTask
@ -151,4 +158,7 @@ internal abstract class RoomModule {
@Binds
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
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases
?: emptyList()
.orEmpty()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")

View File

@ -143,7 +143,7 @@ class DraftRepository @Inject constructor(private val monarchy: Monarchy) {
}
)
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
}
/** See [im.vector.matrix.android.api.session.room.sender.SenderInfo.disambiguatedDisplayName] */
private fun resolveRoomMemberName(roomMemberSummary: RoomMemberSummaryEntity?,
roomMemberHelper: RoomMemberHelper): String? {
if (roomMemberSummary == null) return null

View File

@ -111,7 +111,7 @@ internal class DefaultReadService @AssistedInject constructor(
{ readReceiptsSummaryMapper.map(it) }
)
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 ->
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>?> {

View File

@ -68,31 +68,40 @@ internal class DefaultSendService @AssistedInject constructor(
private val workerFutureListenerExecutor = Executors.newSingleThreadExecutor()
override fun sendTextMessage(text: CharSequence, msgType: String, autoMarkdown: Boolean): Cancelable {
val event = localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown).also {
createLocalEcho(it)
return localEchoEventFactory.createTextEvent(roomId, msgType, text, autoMarkdown)
.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 {
val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also {
createLocalEcho(it)
}
return sendEvent(event)
return localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendPoll(question: String, options: List<OptionItem>): Cancelable {
val event = localEchoEventFactory.createPollEvent(roomId, question, options).also {
createLocalEcho(it)
}
return sendEvent(event)
return localEchoEventFactory.createPollEvent(roomId, question, options)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable {
val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also {
createLocalEcho(it)
}
return sendEvent(event)
return localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
private fun sendEvent(event: Event): Cancelable {
@ -119,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
override fun redactEvent(event: Event, reason: String?): Cancelable {
// TODO manage media/attachements?
val redactWork = createRedactEventWork(event, reason)
return timelineSendEventWorkCommon.postWork(roomId, redactWork)
return createRedactEventWork(event, reason)
.let { timelineSendEventWorkCommon.postWork(roomId, it) }
}
override fun resendTextMessage(localEcho: TimelineEvent): Cancelable? {
@ -263,31 +272,30 @@ internal class DefaultSendService @AssistedInject constructor(
private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
// Same parameter
val params = EncryptEventWorker.Params(sessionId, event)
val sendWorkData = WorkerParamsFactory.toData(params)
return workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
return EncryptEventWorker.Params(sessionId, event)
.let { WorkerParamsFactory.toData(it) }
.let {
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
.setConstraints(WorkManagerProvider.workConstraints)
.setInputData(sendWorkData)
.setInputData(it)
.startChain(startChain)
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
.build()
}
}
private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest {
val sendContentWorkerParams = SendEventWorker.Params(sessionId, event)
val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<SendEventWorker>(sendWorkData, startChain)
return SendEventWorker.Params(sessionId, event)
.let { WorkerParamsFactory.toData(it) }
.let { timelineSendEventWorkCommon.createWork<SendEventWorker>(it, startChain) }
}
private fun createRedactEventWork(event: Event, reason: String?): OneTimeWorkRequest {
val redactEvent = localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason).also {
createLocalEcho(it)
}
val sendContentWorkerParams = RedactEventWorker.Params(sessionId, redactEvent.eventId!!, roomId, event.eventId, reason)
val redactWorkData = WorkerParamsFactory.toData(sendContentWorkerParams)
return timelineSendEventWorkCommon.createWork<RedactEventWorker>(redactWorkData, true)
return localEchoEventFactory.createRedactEvent(roomId, event.eventId!!, reason)
.also { createLocalEcho(it) }
.let { RedactEventWorker.Params(sessionId, it.eventId!!, roomId, event.eventId, reason) }
.let { WorkerParamsFactory.toData(it) }
.let { timelineSendEventWorkCommon.createWork<RedactEventWorker>(it, true) }
}
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.MessageAudioContent
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.MessageFormat
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.getLastMessageContent
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.room.send.pills.TextPillsUtils
import im.vector.matrix.android.internal.task.TaskExecutor
@ -84,6 +86,7 @@ internal class LocalEchoEventFactory @Inject constructor(
) {
// TODO Inject
private val parser = Parser.builder().build()
// TODO Inject
private val renderer = HtmlRenderer.builder().build()
@ -102,8 +105,15 @@ internal class LocalEchoEventFactory @Inject constructor(
val document = parser.parse(source)
val htmlText = renderer.render(document)
if (isFormattedTextPertinent(source, htmlText)) {
return TextContent(text.toString(), htmlText)
// Cleanup extra paragraph
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 {
// Try to detect pills
@ -192,7 +202,7 @@ internal class LocalEchoEventFactory @Inject constructor(
permalink,
stringProvider.getString(R.string.message_reply_to_prefix),
userLink,
originalEvent.getDisambiguatedDisplayName(),
originalEvent.senderInfo.disambiguatedDisplayName,
body.takeFormatted(),
createTextContent(newBodyText, newBodyAutoMarkdown).takeFormatted()
)
@ -433,10 +443,8 @@ internal class LocalEchoEventFactory @Inject constructor(
MessageType.MSGTYPE_TEXT,
MessageType.MSGTYPE_NOTICE -> {
var formattedText: String? = null
if (content is MessageTextContent) {
if (content.format == MessageFormat.FORMAT_MATRIX_HTML) {
formattedText = content.formattedBody
}
if (content is MessageContentWithFormattedBody) {
formattedText = content.matrixFormattedBody
}
val isReply = content.isReply() || originalContent.isReply()
return if (isReply) {

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room.timeline
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.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
@ -71,6 +72,7 @@ internal class DefaultTimeline(
private val realmConfiguration: RealmConfiguration,
private val taskExecutor: TaskExecutor,
private val contextOfEventTask: GetContextOfEventTask,
private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
private val paginationTask: PaginationTask,
private val timelineEventMapper: TimelineEventMapper,
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
*/
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() {
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) {
// 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) {
val token = getTokenLive(direction)
val currentChunk = getLiveChunk()
val token = if (direction == Timeline.Direction.BACKWARDS) currentChunk?.prevToken else currentChunk?.nextToken
if (token == null) {
if (direction == Timeline.Direction.FORWARDS && currentChunk?.hasBeenALastForwardChunk().orFalse()) {
// We are in the case that next event exists, but we do not know the next token.
// Fetch (again) the last event to get a nextToken
val lastKnownEventId = nonFilteredEvents.firstOrNull()?.eventId
if (lastKnownEventId == null) {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
return
}
val params = PaginationTask.Params(roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
} else {
val params = FetchNextTokenAndPaginateTask.Params(
roomId = roomId,
limit = limit,
lastKnownEventId = lastKnownEventId
)
cancelableBag += fetchNextTokenAndPaginateTask
.configureWith(params) {
this.callback = 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")
}
}
this.callback = createPaginationCallback(limit, direction)
}
.executeBy(taskExecutor)
}
} else {
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
}
} else {
val params = PaginationTask.Params(
roomId = roomId,
from = token,
direction = direction.toPaginationDirection(),
limit = limit
)
Timber.v("Should fetch $limit items $direction")
cancelableBag += paginationTask
.configureWith(params) {
this.callback = createPaginationCallback(limit, direction)
}
.executeBy(taskExecutor)
}
}
// For debug purpose only
private fun dumpAndLogChunks() {
val liveChunk = getLiveChunk()
Timber.w("Live chunk: $liveChunk")
Realm.getInstance(realmConfiguration).use { realm ->
ChunkEntity.where(realm, roomId).findAll()
.also { Timber.w("Found ${it.size} chunks") }
.forEach {
Timber.w("")
Timber.w("ChunkEntity: $it")
Timber.w("prevToken: ${it.prevToken}")
Timber.w("nextToken: ${it.nextToken}")
Timber.w("isLastBackward: ${it.isLastBackward}")
Timber.w("isLastForward: ${it.isLastForward}")
it.timelineEvents.forEach { tle ->
Timber.w(" TLE: ${tle.root?.content}")
}
}
}
}
/**
* This has to be called on TimelineThread as it access realm live results
* This has to be called on TimelineThread as it accesses realm live results
*/
private fun getTokenLive(direction: Timeline.Direction): String? {
val chunkEntity = getLiveChunk() ?: return null
return if (direction == Timeline.Direction.BACKWARDS) chunkEntity.prevToken else chunkEntity.nextToken
}
/**
* This has to be called on TimelineThread as it access realm live results
* This has to be called on TimelineThread as it accesses realm live results
* Return the current Chunk
*/
private fun getLiveChunk(): ChunkEntity? {
return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull()
}
/**
* This has to be called on TimelineThread as it access realm live results
* @return number of items who have been added
* This has to be called on TimelineThread as it accesses realm live results
* @return the number of items who have been added
*/
private fun buildTimelineEvents(startDisplayIndex: Int?,
direction: Timeline.Direction,
@ -618,6 +642,8 @@ internal class DefaultTimeline(
}
val time = System.currentTimeMillis() - start
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
}
@ -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,
direction: Timeline.Direction,
@ -713,6 +739,32 @@ internal class DefaultTimeline(
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 ***************************************************************************
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 eventDecryptor: TimelineEventDecryptor,
private val paginationTask: PaginationTask,
private val fetchNextTokenAndPaginateTask: FetchNextTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper
) : TimelineService {
@ -63,7 +64,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
settings = settings,
hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings),
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 events: 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.find
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.findLastForwardChunkOfRoom
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.where
@ -149,7 +149,7 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
}
?: ChunkEntity.create(realm, prevToken, nextToken)
if (receivedChunk.events.isEmpty() && receivedChunk.end == receivedChunk.start) {
if (receivedChunk.events.isEmpty() && !receivedChunk.hasMore()) {
handleReachEnd(realm, roomId, direction, currentChunk)
} else {
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) {
Timber.v("Reach end of $roomId")
if (direction == PaginationDirection.FORWARDS) {
val currentLiveChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)
if (currentChunk != currentLiveChunk) {
val currentLastForwardChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomId)
if (currentChunk != currentLastForwardChunk) {
currentChunk.isLastForward = true
currentLiveChunk?.deleteOnCascade()
currentLastForwardChunk?.deleteOnCascade()
RoomSummaryEntity.where(realm, roomId).findFirst()?.apply {
latestPreviewableEvent = TimelineEventEntity.latestEvent(
realm,
@ -224,10 +224,13 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
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)
Timber.d("Found ${chunks.size} chunks containing at least one of the eventIds")
val chunksToDelete = ArrayList<ChunkEntity>()
chunks.forEach {
if (it != currentChunk) {
Timber.d("Merge $it")
currentChunk.merge(roomId, it, direction)
chunksToDelete.add(it)
}
@ -246,6 +249,8 @@ internal class TokenChunkEventPersistor @Inject constructor(private val monarchy
)
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
}
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.query.copyToRealmOrIgnore
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.getOrNull
import im.vector.matrix.android.internal.database.query.where
@ -220,12 +220,13 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
prevToken: String? = null,
isLimited: Boolean = true,
syncLocalTimestampMillis: Long): ChunkEntity {
val lastChunk = ChunkEntity.findLastLiveChunkFromRoom(realm, roomEntity.roomId)
val lastChunk = ChunkEntity.findLastForwardChunkOfRoom(realm, roomEntity.roomId)
val chunkEntity = if (!isLimited && lastChunk != null) {
lastChunk
} else {
realm.createObject<ChunkEntity>().apply { this.prevToken = prevToken }
}
// Only one chunk has isLastForward set to true
lastChunk?.isLastForward = false
chunkEntity.isLastForward = true

View File

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

View File

@ -41,5 +41,5 @@ internal abstract class AccountDataModule {
abstract fun bindSaveBreadcrumbsTask(task: DefaultSaveBreadcrumbsTask): SaveBreadcrumbsTask
@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="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_you">%1$s invited you</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 work_version = '2.3.3'
def arch_version = '2.1.0'
def lifecycle_version = '2.2.0'
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
@ -282,6 +283,7 @@ dependencies {
implementation "com.gabrielittner.threetenbp:lazythreetenbp:0.7.0"
implementation "com.squareup.moshi:moshi-adapters:$moshi_version"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi_version"
// Log

View File

@ -45,11 +45,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context,
if (time == null) {
return ""
}
return DateUtils.getRelativeDateTimeString(context,
return DateUtils.getRelativeDateTimeString(
context,
time,
DateUtils.DAY_IN_MILLIS,
2 * DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
).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.members.RoomMemberListFragment
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.VectorSettingsHelpAboutFragment
import im.vector.riotx.features.settings.VectorSettingsLabsFragment
@ -310,6 +313,21 @@ interface FragmentModule {
@FragmentKey(RoomMemberListFragment::class)
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
@IntoMap
@FragmentKey(RoomSettingsFragment::class)

View File

@ -75,8 +75,8 @@ abstract class VectorModule {
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter
abstract fun bindErrorFormatter(formatter: DefaultErrorFormatter): ErrorFormatter
@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
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.RecyclerView
import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyVisibilityTracker
import im.vector.riotx.R
import im.vector.riotx.features.themes.ThemeUtils
@ -61,3 +62,5 @@ fun RecyclerView.configureWith(epoxyController: EpoxyController,
fun RecyclerView.cleanup() {
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.view.View
import android.widget.FrameLayout
import androidx.core.view.isVisible
import im.vector.riotx.R
import kotlinx.android.synthetic.main.view_state.view.*
@ -31,6 +32,7 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
object Content : State()
object Loading : 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()
}
@ -59,34 +61,21 @@ class StateView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
}
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) {
is State.Content -> {
progressBar.visibility = View.INVISIBLE
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.Content -> Unit
is State.Loading -> Unit
is State.Empty -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.INVISIBLE
emptyView.visibility = View.VISIBLE
emptyImageView.setImageDrawable(newState.image)
emptyMessageView.text = newState.message
emptyTitleView.text = newState.title
contentView?.visibility = View.INVISIBLE
}
is State.Error -> {
progressBar.visibility = View.INVISIBLE
errorView.visibility = View.VISIBLE
emptyView.visibility = View.INVISIBLE
errorMessageView.text = newState.message
contentView?.visibility = View.INVISIBLE
}
}
}

View File

@ -16,5 +16,7 @@
package im.vector.riotx.core.ui.model
import androidx.annotation.Px
// 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.util.TypedValue
import androidx.annotation.Px
import javax.inject.Inject
class DimensionConverter @Inject constructor(val resources: Resources) {
@Px
fun dpToPx(dp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
@ -29,6 +31,7 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
).toInt()
}
@Px
fun spToPx(sp: Int): Int {
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
@ -36,4 +39,8 @@ class DimensionConverter @Inject constructor(val resources: Resources) {
resources.displayMetrics
).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.putExtra(Intent.EXTRA_STREAM, mediaUri)
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> {
return intent.getParcelableArrayListExtra(ATTACHMENTS_PREVIEW_RESULT) ?: emptyList()
return intent.getParcelableArrayListExtra<ContentAttachmentData>(ATTACHMENTS_PREVIEW_RESULT).orEmpty()
}
fun getKeepOriginalSize(intent: Intent): Boolean {

View File

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

View File

@ -123,12 +123,12 @@ class HomeDetailFragment @Inject constructor(
?.navigator
?.requestSessionVerification(requireContext(), newest.deviceId ?: "")
unknownDeviceDetectorSharedViewModel.handle(
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) } ?: emptyList())
UnknownDeviceDetectorSharedViewModel.Action.IgnoreDevice(newest.deviceId?.let { listOf(it) }.orEmpty())
)
}
dismissedAction = Runnable {
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.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.text.Spannable
@ -30,12 +29,10 @@ import android.view.HapticFeedbackConstants
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.Window
import android.widget.Toast
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityOptionsCompat
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.text.buildSpannedString
@ -49,7 +46,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.Async
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.setTextOrHide
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.glide.GlideApp
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.invite.VectorInviteView
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.VideoMediaViewerActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.permalink.NavigationInterceptor
import im.vector.riotx.features.permalink.PermalinkHandler
@ -469,7 +464,7 @@ class RoomDetailFragment @Inject constructor(
autoCompleter.enterSpecialMode()
// switch to expanded bar
composerLayout.composerRelatedMessageTitle.apply {
text = event.getDisambiguatedDisplayName()
text = event.senderInfo.disambiguatedDisplayName
setTextColor(ContextCompat.getColor(requireContext(), getColorFromUserId(event.root.senderId)))
}
@ -488,11 +483,7 @@ class RoomDetailFragment @Inject constructor(
composerLayout.composerRelatedMessageActionIcon.setImageDrawable(ContextCompat.getDrawable(requireContext(), iconRes))
composerLayout.sendButton.contentDescription = getString(descriptionRes)
avatarRenderer.render(
MatrixItem.UserItem(event.root.senderId
?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
composerLayout.composerRelatedMessageAvatar
)
avatarRenderer.render(event.senderInfo.toMatrixItem(), composerLayout.composerRelatedMessageAvatar)
composerLayout.expand {
if (isAdded) {
@ -549,8 +540,7 @@ class RoomDetailFragment @Inject constructor(
timelineEventController.callback = this
timelineEventController.timeline = roomDetailViewModel.timeline
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
recyclerView.trackItemsVisibilityChange()
layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
val stateRestorer = LayoutManagerStateRestorer(layoutManager).register()
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager, timelineEventController)
@ -1004,31 +994,14 @@ class RoomDetailFragment @Inject constructor(
}
override fun onImageMessageClicked(messageImageContent: MessageImageInfoContent, mediaData: ImageContentRenderer.Data, view: View) {
// TODO Use navigator
val intent = ImageMediaViewerActivity.newIntent(vectorBaseActivity, mediaData, ViewCompat.getTransitionName(view))
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) ?: ""))
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
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) {
// TODO Use navigator
val intent = VideoMediaViewerActivity.newIntent(vectorBaseActivity, mediaData)
startActivity(intent)
navigator.openVideoViewer(requireActivity(), mediaData)
}
override fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) {

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