mirror of
https://github.com/element-hq/element-android
synced 2024-12-18 07:12:47 +03:00
Merge branch 'develop' into feature/integration_manager
This commit is contained in:
commit
7409fde650
147 changed files with 3392 additions and 543 deletions
11
CHANGES.md
11
CHANGES.md
|
@ -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)
|
||||
===================================================
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,17 +53,19 @@ 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)
|
||||
}
|
||||
|
||||
val room = aliceSession.getRoom(roomId)!!
|
||||
if (encryptedRoom) {
|
||||
val room = aliceSession.getRoom(roomId)!!
|
||||
|
||||
mTestHelper.doSync<Unit> {
|
||||
room.enableEncryption(callback = it)
|
||||
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
|
||||
|
||||
|
|
|
@ -468,14 +468,19 @@ 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 -> {
|
||||
aliceSASLatch.countDown()
|
||||
if (matchOnce) {
|
||||
matchOnce = false
|
||||
aliceSASLatch.countDown()
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
@ -485,14 +490,23 @@ 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 -> {
|
||||
tx.performAccept()
|
||||
if (acceptOnce) {
|
||||
acceptOnce = false
|
||||
tx.performAccept()
|
||||
}
|
||||
}
|
||||
IncomingSasVerificationTransaction.UxState.SHOW_SAS -> {
|
||||
tx.userHasVerifiedShortCode()
|
||||
if (matchOnce) {
|
||||
matchOnce = false
|
||||
tx.userHasVerifiedShortCode()
|
||||
}
|
||||
}
|
||||
IncomingSasVerificationTransaction.UxState.VERIFIED -> {
|
||||
bobSASLatch.countDown()
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineBackToPreviousLastForwardTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink of an
|
||||
* even contained in a previous lastForward chunk, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun backToPreviousLastForwardTest() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
var roomCreationEventId: String? = null
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
roomCreationEventId = snapshot.lastOrNull()?.root?.eventId
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob join event)
|
||||
snapshot.size == 8
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val messageRoot = "First messages from Alice"
|
||||
|
||||
// Alice sends 30 messages
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
messageRoot,
|
||||
30)
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice.
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(messageRoot).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob navigate to the first event (room creation event), so inside the previous last forward chunk
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// The event is in db, so it is fetch and auto pagination occurs, half of the number of events we have for this chunk (?)
|
||||
snapshot.size == 4
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, which is already in the database, so pagination should start automatically
|
||||
assertTrue(roomFromBobPOV.getTimeLineEvent(roomCreationEventId!!) != null)
|
||||
|
||||
bobTimeline.restartWithEventId(roomCreationEventId)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob scroll to the future
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 8 for room creation item, and 30 for the forward pagination
|
||||
&& snapshot.size == 38
|
||||
&& snapshot.checkSendOrder(messageRoot, 30, 0)
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelineForwardPaginationTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we click to permalink, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun forwardPaginationTest() {
|
||||
val numberOfMessagesToSend = 90
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
|
||||
// Alice sends X messages
|
||||
val message = "Message from Alice"
|
||||
val sentMessages = commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
message,
|
||||
numberOfMessagesToSend)
|
||||
|
||||
// Alice clear the cache
|
||||
commonTestHelper.doSync<Unit> {
|
||||
aliceSession.clearCache(it)
|
||||
}
|
||||
|
||||
// And restarts the sync
|
||||
aliceSession.startSync(true)
|
||||
|
||||
val aliceTimeline = roomFromAlicePOV.createTimeline(null, TimelineSettings(30))
|
||||
aliceTimeline.start()
|
||||
|
||||
// Alice sees the 10 last message of the room, and can only navigate BACKWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages of the initial sync
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(message).orFalse() }
|
||||
}
|
||||
|
||||
// Open the timeline at last sent message
|
||||
aliceTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Alice navigates to the first message of the room, which is not in its database. A GET /context is performed
|
||||
// Then she can paginate BACKWARD and FORWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// The event is not in db, so it is fetch alone
|
||||
snapshot.size == 1
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith("Message from Alice").orFalse() }
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event
|
||||
aliceTimeline.restartWithEventId(sentMessages.last().eventId)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
}
|
||||
|
||||
// Alice paginates BACKWARD and FORWARD of 50 events each
|
||||
// Then she can only navigate FORWARD
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
|
||||
// Alice can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 6 for room creation item (backward pagination), 1 for the context, and 50 for the forward pagination
|
||||
&& snapshot.size == 6 + 1 + 50
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event
|
||||
// We ask to load event backward and forward
|
||||
aliceTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Alice paginates once again FORWARD for 50 events
|
||||
// All the timeline is retrieved, she cannot paginate anymore in both direction
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val aliceEventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Alice timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root.content}")
|
||||
}
|
||||
// 6 for room creation item (backward pagination),and numberOfMessagesToSend (all the message of the room)
|
||||
snapshot.size == 6 + numberOfMessagesToSend
|
||||
&& snapshot.checkSendOrder(message, numberOfMessagesToSend, 0)
|
||||
}
|
||||
|
||||
aliceTimeline.addListener(aliceEventsListener)
|
||||
|
||||
// Ask for a forward pagination
|
||||
aliceTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
aliceTimeline.removeAllListeners()
|
||||
|
||||
// The timeline is fully loaded
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
aliceTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
aliceTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,241 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.matrix.android.session.room.timeline
|
||||
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.session.events.model.EventType
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.model.message.MessageContent
|
||||
import im.vector.matrix.android.api.session.room.timeline.Timeline
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.CryptoTestHelper
|
||||
import im.vector.matrix.android.common.checkSendOrder
|
||||
import org.amshove.kluent.shouldBeFalse
|
||||
import org.amshove.kluent.shouldBeTrue
|
||||
import org.junit.FixMethodOrder
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.JUnit4
|
||||
import org.junit.runners.MethodSorters
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.CountDownLatch
|
||||
|
||||
@RunWith(JUnit4::class)
|
||||
@FixMethodOrder(MethodSorters.JVM)
|
||||
class TimelinePreviousLastForwardTest : InstrumentedTest {
|
||||
|
||||
private val commonTestHelper = CommonTestHelper(context())
|
||||
private val cryptoTestHelper = CryptoTestHelper(commonTestHelper)
|
||||
|
||||
/**
|
||||
* This test ensure that if we have a chunk in the timeline which is due to a sync, and we click to permalink, we will be able to go back to the live
|
||||
*/
|
||||
@Test
|
||||
fun previousLastForwardTest() {
|
||||
val cryptoTestData = cryptoTestHelper.doE2ETestWithAliceAndBobInARoom(false)
|
||||
|
||||
val aliceSession = cryptoTestData.firstSession
|
||||
val bobSession = cryptoTestData.secondSession!!
|
||||
val aliceRoomId = cryptoTestData.roomId
|
||||
|
||||
aliceSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
bobSession.cryptoService().setWarnOnUnknownDevices(false)
|
||||
|
||||
val roomFromAlicePOV = aliceSession.getRoom(aliceRoomId)!!
|
||||
val roomFromBobPOV = bobSession.getRoom(aliceRoomId)!!
|
||||
|
||||
val bobTimeline = roomFromBobPOV.createTimeline(null, TimelineSettings(30))
|
||||
bobTimeline.start()
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 8 first messages of the initial sync (room creation and bob invite and join events)
|
||||
snapshot.size == 8
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val firstMessage = "First messages from Alice"
|
||||
// Alice sends 30 messages
|
||||
val firstMessageFromAliceId = commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
firstMessage,
|
||||
30)
|
||||
.last()
|
||||
.eventId
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(firstMessage).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob stop to sync
|
||||
bobSession.stopSync()
|
||||
|
||||
val secondMessage = "Second messages from Alice"
|
||||
// Alice sends again 30 messages
|
||||
commonTestHelper.sendTextMessage(
|
||||
roomFromAlicePOV,
|
||||
secondMessage,
|
||||
30)
|
||||
|
||||
// Bob start to sync
|
||||
bobSession.startSync(true)
|
||||
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Ok, we have the 10 last messages from Alice. This will be our future previous lastForward chunk
|
||||
snapshot.size == 10
|
||||
&& snapshot.all { it.root.content.toModel<MessageContent>()?.body?.startsWith(secondMessage).orFalse() }
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob navigate to the first message sent from Alice
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// The event is not in db, so it is fetch
|
||||
snapshot.size == 1
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Restart the timeline to the first sent event, and paginate in both direction
|
||||
bobTimeline.restartWithEventId(firstMessageFromAliceId)
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeTrue()
|
||||
}
|
||||
|
||||
// Paginate in both direction
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
snapshot.size == 8 + 1 + 35
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
// Paginate in both direction
|
||||
bobTimeline.paginate(Timeline.Direction.BACKWARDS, 50)
|
||||
// Ensure the chunk in the middle is included in the next pagination
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 35)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeTrue()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
// Bob scroll to the future, till the live
|
||||
run {
|
||||
val lock = CountDownLatch(1)
|
||||
val eventsListener = commonTestHelper.createEventListener(lock) { snapshot ->
|
||||
Timber.e("Bob timeline updated: with ${snapshot.size} events:")
|
||||
snapshot.forEach {
|
||||
Timber.w(" event ${it.root}")
|
||||
}
|
||||
|
||||
// Bob can see the first event of the room (so Back pagination has worked)
|
||||
snapshot.lastOrNull()?.root?.getClearType() == EventType.STATE_ROOM_CREATE
|
||||
// 8 for room creation item 60 message from Alice
|
||||
&& snapshot.size == 8 + 60
|
||||
&& snapshot.checkSendOrder(secondMessage, 30, 0)
|
||||
&& snapshot.checkSendOrder(firstMessage, 30, 30)
|
||||
}
|
||||
|
||||
bobTimeline.addListener(eventsListener)
|
||||
|
||||
bobTimeline.paginate(Timeline.Direction.FORWARDS, 50)
|
||||
|
||||
commonTestHelper.await(lock)
|
||||
bobTimeline.removeAllListeners()
|
||||
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.FORWARDS).shouldBeFalse()
|
||||
bobTimeline.hasMoreToLoad(Timeline.Direction.BACKWARDS).shouldBeFalse()
|
||||
}
|
||||
|
||||
bobTimeline.dispose()
|
||||
|
||||
cryptoTestData.cleanUp(commonTestHelper)
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 }
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>> {
|
||||
|
|
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -198,18 +198,8 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
// using the result as the shared secret.
|
||||
|
||||
getSAS().setTheirPublicKey(otherKey)
|
||||
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
|
||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
||||
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.start message,
|
||||
// - the device ID of the device that sent the m.key.verification.start message,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
|
||||
|
||||
shortCodeBytes = calculateSASBytes()
|
||||
|
||||
if (BuildConfig.LOG_PRIVATE_DATA) {
|
||||
Timber.v("************ BOB CODE ${getDecimalCodeRepresentation(shortCodeBytes!!)}")
|
||||
|
@ -219,6 +209,35 @@ internal class DefaultIncomingSASDefaultVerificationTransaction(
|
|||
state = VerificationTxState.ShortCodeReady
|
||||
}
|
||||
|
||||
private fun calculateSASBytes(): ByteArray {
|
||||
when (accepted?.keyAgreementProtocol) {
|
||||
KEY_AGREEMENT_V1 -> {
|
||||
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
|
||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
||||
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.start message,
|
||||
// - the device ID of the device that sent the m.key.verification.start message,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$otherUserId$otherDeviceId$userId$deviceId$transactionId"
|
||||
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
return getSAS().generateShortCode(sasInfo, 6)
|
||||
}
|
||||
KEY_AGREEMENT_V2 -> {
|
||||
// Adds the SAS public key, and separate by |
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$otherUserId|$otherDeviceId|$otherKey|$userId|$deviceId|${getSAS().publicKey}|$transactionId"
|
||||
return getSAS().generateShortCode(sasInfo, 6)
|
||||
}
|
||||
else -> {
|
||||
// Protocol has been checked earlier
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
|
||||
Timber.v("## SAS I: received mac for request id:$transactionId")
|
||||
// Check for state?
|
||||
|
|
|
@ -193,18 +193,7 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
|
||||
if (accepted!!.commitment.equals(otherCommitment)) {
|
||||
getSAS().setTheirPublicKey(otherKey)
|
||||
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
|
||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
||||
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.start message,
|
||||
// - the device ID of the device that sent the m.key.verification.start message,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
shortCodeBytes = getSAS().generateShortCode(sasInfo, 6)
|
||||
shortCodeBytes = calculateSASBytes()
|
||||
state = VerificationTxState.ShortCodeReady
|
||||
} else {
|
||||
// bad commitment
|
||||
|
@ -212,14 +201,45 @@ internal class DefaultOutgoingSASDefaultVerificationTransaction(
|
|||
}
|
||||
}
|
||||
|
||||
private fun calculateSASBytes(): ByteArray {
|
||||
when (accepted?.keyAgreementProtocol) {
|
||||
KEY_AGREEMENT_V1 -> {
|
||||
// (Note: In all of the following HKDF is as defined in RFC 5869, and uses the previously agreed-on hash function as the hash function,
|
||||
// the shared secret as the input keying material, no salt, and with the input parameter set to the concatenation of:
|
||||
// - the string “MATRIX_KEY_VERIFICATION_SAS”,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.start message,
|
||||
// - the device ID of the device that sent the m.key.verification.start message,
|
||||
// - the Matrix ID of the user who sent the m.key.verification.accept message,
|
||||
// - he device ID of the device that sent the m.key.verification.accept message
|
||||
// - the transaction ID.
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS$userId$deviceId$otherUserId$otherDeviceId$transactionId"
|
||||
|
||||
// decimal: generate five bytes by using HKDF.
|
||||
// emoji: generate six bytes by using HKDF.
|
||||
return getSAS().generateShortCode(sasInfo, 6)
|
||||
}
|
||||
KEY_AGREEMENT_V2 -> {
|
||||
// Adds the SAS public key, and separate by |
|
||||
val sasInfo = "MATRIX_KEY_VERIFICATION_SAS|$userId|$deviceId|${getSAS().publicKey}|$otherUserId|$otherDeviceId|$otherKey|$transactionId"
|
||||
return getSAS().generateShortCode(sasInfo, 6)
|
||||
}
|
||||
else -> {
|
||||
// Protocol has been checked earlier
|
||||
throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onKeyVerificationMac(vMac: ValidVerificationInfoMac) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -60,10 +60,9 @@ internal fun ChunkEntity.merge(roomId: String, chunkToMerge: ChunkEntity, direct
|
|||
chunkToMerge.stateEvents.forEach { stateEvent ->
|
||||
addStateEvent(roomId, stateEvent, direction)
|
||||
}
|
||||
return eventsToMerge
|
||||
.forEach {
|
||||
addTimelineEventFromMerge(localRealm, it, direction)
|
||||
}
|
||||
eventsToMerge.forEach {
|
||||
addTimelineEventFromMerge(localRealm, it, direction)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun ChunkEntity.addStateEvent(roomId: String, stateEvent: EventEntity, direction: PaginationDirection) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
senderAvatar = timelineEventEntity.senderAvatar,
|
||||
senderInfo = SenderInfo(
|
||||
userId = timelineEventEntity.root?.sender ?: "",
|
||||
displayName = timelineEventEntity.senderName,
|
||||
isUniqueDisplayName = timelineEventEntity.isUniqueDisplayName,
|
||||
avatarUrl = timelineEventEntity.senderAvatar
|
||||
),
|
||||
readReceipts = readReceipts
|
||||
?.distinctBy {
|
||||
it.user
|
||||
}?.sortedByDescending {
|
||||
it.originServerTs
|
||||
} ?: emptyList()
|
||||
}.orEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = ""
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 = "|")
|
||||
|
|
|
@ -143,7 +143,7 @@ class DraftRepository @Inject constructor(private val monarchy: Monarchy) {
|
|||
}
|
||||
)
|
||||
return Transformations.map(liveData) {
|
||||
it.firstOrNull() ?: emptyList()
|
||||
it.firstOrNull().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -111,7 +111,7 @@ internal class DefaultReadService @AssistedInject constructor(
|
|||
{ readReceiptsSummaryMapper.map(it) }
|
||||
)
|
||||
return Transformations.map(liveRealmData) {
|
||||
it.firstOrNull() ?: emptyList()
|
||||
it.firstOrNull().orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>?> {
|
||||
|
|
|
@ -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>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.setInputData(sendWorkData)
|
||||
.startChain(startChain)
|
||||
.setBackoffCriteria(BackoffPolicy.LINEAR, WorkManagerProvider.BACKOFF_DELAY, TimeUnit.MILLISECONDS)
|
||||
.build()
|
||||
return EncryptEventWorker.Params(sessionId, event)
|
||||
.let { WorkerParamsFactory.toData(it) }
|
||||
.let {
|
||||
workManagerProvider.matrixOneTimeWorkRequestBuilder<EncryptEventWorker>()
|
||||
.setConstraints(WorkManagerProvider.workConstraints)
|
||||
.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>,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
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
|
||||
.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)
|
||||
}
|
||||
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) }
|
||||
} else {
|
||||
val params = FetchNextTokenAndPaginateTask.Params(
|
||||
roomId = roomId,
|
||||
limit = limit,
|
||||
lastKnownEventId = lastKnownEventId
|
||||
)
|
||||
cancelableBag += fetchNextTokenAndPaginateTask
|
||||
.configureWith(params) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(failure: Throwable) {
|
||||
updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) }
|
||||
postSnapshot()
|
||||
Timber.v("Failure fetching $limit items $direction from pagination request")
|
||||
// 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}")
|
||||
}
|
||||
}
|
||||
}
|
||||
.executeBy(taskExecutor)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This has to be called on TimelineThread as it access realm live results
|
||||
* This has to be called on TimelineThread as it accesses realm live results
|
||||
*/
|
||||
|
||||
private fun getTokenLive(direction: Timeline.Direction): String? {
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -23,4 +23,6 @@ internal interface TokenChunkEvent {
|
|||
val end: String?
|
||||
val events: List<Event>
|
||||
val stateEvents: List<Event>
|
||||
|
||||
fun hasMore() = start != end
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
||||
if (currentChunk.isValid) {
|
||||
RoomEntity.where(realm, roomId).findFirst()?.addOrUpdate(currentChunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ data class TermsResponse(
|
|||
version = tos[VERSION] as? String
|
||||
)
|
||||
}
|
||||
}?.filterNotNull() ?: emptyList()
|
||||
}?.filterNotNull().orEmpty()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,11 +45,12 @@ class VectorDateFormatter @Inject constructor(private val context: Context,
|
|||
if (time == null) {
|
||||
return ""
|
||||
}
|
||||
return DateUtils.getRelativeDateTimeString(context,
|
||||
time,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
2 * DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY
|
||||
return DateUtils.getRelativeDateTimeString(
|
||||
context,
|
||||
time,
|
||||
DateUtils.DAY_IN_MILLIS,
|
||||
2 * DateUtils.DAY_IN_MILLIS,
|
||||
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
|
||||
).toString()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -256,7 +256,11 @@ fun shareMedia(context: Context, file: File, mediaMimeType: String?) {
|
|||
sendIntent.type = mediaMimeType
|
||||
sendIntent.putExtra(Intent.EXTRA_STREAM, mediaUri)
|
||||
|
||||
context.startActivity(sendIntent)
|
||||
try {
|
||||
context.startActivity(sendIntent)
|
||||
} catch (activityNotFoundException: ActivityNotFoundException) {
|
||||
context.toast(R.string.error_no_external_application_found)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
navigator.openImageViewer(requireActivity(), mediaData, view) { pairs ->
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
}
|
||||
pairs.add(Pair(view, ViewCompat.getTransitionName(view) ?: ""))
|
||||
pairs.add(Pair(roomToolbar, ViewCompat.getTransitionName(roomToolbar) ?: ""))
|
||||
pairs.add(Pair(composerLayout, ViewCompat.getTransitionName(composerLayout) ?: ""))
|
||||
|
||||
val bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
||||
requireActivity(), *pairs.toTypedArray()).toBundle()
|
||||
startActivity(intent, bundle)
|
||||
}
|
||||
|
||||
override fun onVideoMessageClicked(messageVideoContent: MessageVideoContent, mediaData: VideoContentRenderer.Data, view: View) {
|
||||
// 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
Loading…
Reference in a new issue