diff --git a/CHANGES.md b/CHANGES.md index d083a5f2f5..5b7c04d3ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,28 @@ +Changes in RiotX 0.16.0 (2020-02-14) +=================================================== + +Features ✨: + - Polls and Bot Buttons (MSC 2192 matrix-org/matrix-doc#2192) + +Improvements πŸ™Œ: + - Show confirmation dialog before deleting a message (#967, #1003) + - Open room member profile from reactions list and read receipts list (#875) + +Bugfix πŸ›: + - Fix crash by removing all notifications after clearing cache (#878) + - Fix issue with verification when other client declares it can only show QR code (#988) + - Fix too errors in the code (1941862499c9ec5268cc80882512ced379cafcfd, a250a895fe0a4acf08c671e03434edcd29ccd84f) + +SDK API changes ⚠️: + - Javadoc improved for PushersService + - PushersService.pushers() has been renamed to PushersService.getPushers() + Changes in RiotX 0.15.0 (2020-02-10) =================================================== Improvements πŸ™Œ: - Improve navigation to the timeline (#789, #862) - - Improve network detection. It is now based on the sync request status (#873, #882) + - Improve network detection. It is now based on the sync request status (#873, #882) Other changes: - Support SSO login with Firefox account (#606) @@ -363,15 +382,17 @@ Features ✨: Improvements πŸ™Œ: - -Other changes: - - - Bugfix πŸ›: - Translations πŸ—£: - +SDK API changes ⚠️: + - + Build 🧱: - +Other changes: + - diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt index 6ae2489993..79670bb21e 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/SASTest.kt @@ -66,14 +66,10 @@ class SASTest : InstrumentedTest { val bobVerificationService = bobSession!!.getVerificationService() val bobTxCreatedLatch = CountDownLatch(1) - val bobListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { bobTxCreatedLatch.countDown() } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobVerificationService.addListener(bobListener) @@ -106,9 +102,7 @@ class SASTest : InstrumentedTest { // Let's cancel from alice side val cancelLatch = CountDownLatch(1) - val bobListener2 = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener2 = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if (tx.transactionId == txID) { val immutableState = (tx as SASDefaultVerificationTransaction).state @@ -117,8 +111,6 @@ class SASTest : InstrumentedTest { } } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobVerificationService.addListener(bobListener2) @@ -157,17 +149,13 @@ class SASTest : InstrumentedTest { var cancelReason: CancelCode? = null val cancelLatch = CountDownLatch(1) - val bobListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if (tx.transactionId == tid && tx.state is VerificationTxState.Cancelled) { cancelReason = (tx.state as VerificationTxState.Cancelled).cancelCode cancelLatch.countDown() } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobSession.getVerificationService().addListener(bobListener) @@ -186,16 +174,12 @@ class SASTest : InstrumentedTest { val aliceUserID = aliceSession.myUserId val aliceDevice = aliceSession.getMyDevice().deviceId - val aliceListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { (tx as IncomingSasVerificationTransaction).performAccept() } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } aliceSession.getVerificationService().addListener(aliceListener) @@ -328,7 +312,7 @@ class SASTest : InstrumentedTest { val aliceCreatedLatch = CountDownLatch(2) val aliceCancelledLatch = CountDownLatch(2) val createdTx = mutableListOf() - val aliceListener = object : VerificationService.VerificationListener { + val aliceListener = object : VerificationService.Listener { override fun transactionCreated(tx: VerificationTransaction) { createdTx.add(tx as SASDefaultVerificationTransaction) aliceCreatedLatch.countDown() @@ -339,8 +323,6 @@ class SASTest : InstrumentedTest { aliceCancelledLatch.countDown() } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } aliceVerificationService.addListener(aliceListener) @@ -372,11 +354,7 @@ class SASTest : InstrumentedTest { var startReq: KeyVerificationStart? = null val aliceAcceptedLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.VerificationListener { - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} - - override fun transactionCreated(tx: VerificationTransaction) {} - + val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if ((tx as SASDefaultVerificationTransaction).state === VerificationTxState.OnAccepted) { val at = tx as SASDefaultVerificationTransaction @@ -388,17 +366,13 @@ class SASTest : InstrumentedTest { } aliceVerificationService.addListener(aliceListener) - val bobListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { if ((tx as IncomingSasVerificationTransaction).uxState === IncomingSasVerificationTransaction.UxState.SHOW_ACCEPT) { val at = tx as IncomingSasVerificationTransaction at.performAccept() } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobVerificationService.addListener(bobListener) @@ -433,9 +407,7 @@ class SASTest : InstrumentedTest { val bobVerificationService = bobSession!!.getVerificationService() val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as OutgoingSasVerificationTransaction).uxState when (uxState) { @@ -445,15 +417,11 @@ class SASTest : InstrumentedTest { else -> Unit } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } aliceVerificationService.addListener(aliceListener) val bobSASLatch = CountDownLatch(1) - val bobListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as IncomingSasVerificationTransaction).uxState when (uxState) { @@ -466,8 +434,6 @@ class SASTest : InstrumentedTest { bobSASLatch.countDown() } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobVerificationService.addListener(bobListener) @@ -497,9 +463,7 @@ class SASTest : InstrumentedTest { val bobVerificationService = bobSession!!.getVerificationService() val aliceSASLatch = CountDownLatch(1) - val aliceListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val aliceListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as OutgoingSasVerificationTransaction).uxState when (uxState) { @@ -512,15 +476,11 @@ class SASTest : InstrumentedTest { else -> Unit } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } aliceVerificationService.addListener(aliceListener) val bobSASLatch = CountDownLatch(1) - val bobListener = object : VerificationService.VerificationListener { - override fun transactionCreated(tx: VerificationTransaction) {} - + val bobListener = object : VerificationService.Listener { override fun transactionUpdated(tx: VerificationTransaction) { val uxState = (tx as IncomingSasVerificationTransaction).uxState when (uxState) { @@ -536,8 +496,6 @@ class SASTest : InstrumentedTest { else -> Unit } } - - override fun markedAsManuallyVerified(userId: String, deviceId: String) {} } bobVerificationService.addListener(bobListener) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt new file mode 100644 index 0000000000..61ea0f35b4 --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/verification/qrcode/VerificationTest.kt @@ -0,0 +1,232 @@ +/* + * 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.crypto.verification.qrcode + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import im.vector.matrix.android.InstrumentedTest +import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod +import im.vector.matrix.android.api.session.crypto.sas.VerificationService +import im.vector.matrix.android.common.CommonTestHelper +import im.vector.matrix.android.common.CryptoTestHelper +import im.vector.matrix.android.common.TestConstants +import im.vector.matrix.android.internal.crypto.model.rest.UserPasswordAuth +import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest +import org.amshove.kluent.shouldBe +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.MethodSorters +import java.util.concurrent.CountDownLatch + +@RunWith(AndroidJUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class VerificationTest : InstrumentedTest { + private val mTestHelper = CommonTestHelper(context()) + private val mCryptoTestHelper = CryptoTestHelper(mTestHelper) + + data class ExpectedResult( + val sasIsSupported: Boolean = false, + val otherCanScanQrCode: Boolean = false, + val otherCanShowQrCode: Boolean = false + ) + + private val sas = listOf( + VerificationMethod.SAS + ) + + private val sasShow = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW + ) + + private val sasScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SCAN + ) + + private val sasShowScan = listOf( + VerificationMethod.SAS, + VerificationMethod.QR_CODE_SHOW, + VerificationMethod.QR_CODE_SCAN + ) + + @Test + fun test_aliceAndBob_sas_sas() = doTest( + sas, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_show() = doTest( + sas, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_sas() = doTest( + sasShow, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_sas_scan() = doTest( + sas, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_sas() = doTest( + sasScan, + sas, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_scan_scan() = doTest( + sasScan, + sasScan, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_show() = doTest( + sasShow, + sasShow, + ExpectedResult(sasIsSupported = true), + ExpectedResult(sasIsSupported = true) + ) + + @Test + fun test_aliceAndBob_show_scan() = doTest( + sasShow, + sasScan, + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true) + ) + + @Test + fun test_aliceAndBob_scan_show() = doTest( + sasScan, + sasShow, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanScanQrCode = true) + ) + + @Test + fun test_aliceAndBob_all_all() = doTest( + sasShowScan, + sasShowScan, + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true), + ExpectedResult(sasIsSupported = true, otherCanShowQrCode = true, otherCanScanQrCode = true) + ) + + // TODO Add tests without SAS + + private fun doTest(aliceSupportedMethods: List, + bobSupportedMethods: List, + expectedResultForAlice: ExpectedResult, + expectedResultForBob: ExpectedResult) { + val cryptoTestData = mCryptoTestHelper.doE2ETestWithAliceAndBobInARoom() + + val aliceSession = cryptoTestData.firstSession + val bobSession = cryptoTestData.secondSession!! + + mTestHelper.doSync { callback -> + aliceSession.getCrossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = aliceSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + mTestHelper.doSync { callback -> + bobSession.getCrossSigningService() + .initializeCrossSigning(UserPasswordAuth( + user = bobSession.myUserId, + password = TestConstants.PASSWORD + ), callback) + } + + val aliceVerificationService = aliceSession.getVerificationService() + val bobVerificationService = bobSession.getVerificationService() + + var aliceReadyPendingVerificationRequest: PendingVerificationRequest? = null + var bobReadyPendingVerificationRequest: PendingVerificationRequest? = null + + val latch = CountDownLatch(2) + val aliceListener = object : VerificationService.Listener { + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 4: Alice receive the ready request + if (pr.isReady) { + aliceReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + aliceVerificationService.addListener(aliceListener) + + val bobListener = object : VerificationService.Listener { + override fun verificationRequestCreated(pr: PendingVerificationRequest) { + // Step 2: Bob accepts the verification request + bobVerificationService.readyPendingVerificationInDMs( + bobSupportedMethods, + aliceSession.myUserId, + cryptoTestData.roomId, + pr.transactionId!! + ) + } + + override fun verificationRequestUpdated(pr: PendingVerificationRequest) { + // Step 3: Bob is ready + if (pr.isReady) { + bobReadyPendingVerificationRequest = pr + latch.countDown() + } + } + } + bobVerificationService.addListener(bobListener) + + val bobUserId = bobSession.myUserId + // Step 1: Alice starts a verification request + aliceVerificationService.requestKeyVerificationInDMs(aliceSupportedMethods, bobUserId, cryptoTestData.roomId) + mTestHelper.await(latch) + + aliceReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForAlice.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForAlice.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForAlice.otherCanScanQrCode + } + + bobReadyPendingVerificationRequest!!.let { pr -> + pr.isSasSupported() shouldBe expectedResultForBob.sasIsSupported + pr.otherCanShowQrCode() shouldBe expectedResultForBob.otherCanShowQrCode + pr.otherCanScanQrCode() shouldBe expectedResultForBob.otherCanScanQrCode + } + + cryptoTestData.close() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt index 0dd143f792..1b5f5d3dd6 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/crypto/sas/VerificationService.kt @@ -30,9 +30,9 @@ import im.vector.matrix.android.internal.crypto.verification.PendingVerification */ interface VerificationService { - fun addListener(listener: VerificationListener) + fun addListener(listener: Listener) - fun removeListener(listener: VerificationListener) + fun removeListener(listener: Listener) /** * Mark this device as verified manually @@ -68,11 +68,11 @@ interface VerificationService { otherDevices: List?): PendingVerificationRequest fun declineVerificationRequestInDMs(otherUserId: String, - otherDeviceId: String, transactionId: String, roomId: String) // Only SAS method is supported for the moment + // TODO Parameter otherDeviceId should be removed in this case fun beginKeyVerificationInDMs(method: VerificationMethod, transactionId: String, roomId: String, @@ -95,15 +95,33 @@ interface VerificationService { otherUserId: String, transactionId: String): Boolean - // fun transactionUpdated(tx: SasVerificationTransaction) - - interface VerificationListener { - fun transactionCreated(tx: VerificationTransaction) - fun transactionUpdated(tx: VerificationTransaction) - fun markedAsManuallyVerified(userId: String, deviceId: String) {} - + interface Listener { + /** + * Called when a verification request is created either by the user, or by the other user. + */ fun verificationRequestCreated(pr: PendingVerificationRequest) {} + + /** + * Called when a verification request is updated. + */ fun verificationRequestUpdated(pr: PendingVerificationRequest) {} + + /** + * Called when a transaction is created, either by the user or initiated by the other user. + */ + fun transactionCreated(tx: VerificationTransaction) {} + + /** + * Called when a transaction is updated. You may be interested to track the state of the VerificationTransaction. + */ + fun transactionUpdated(tx: VerificationTransaction) {} + + /** + * Inform the the deviceId of the userId has been marked as manually verified by the SDK. + * It will be called after VerificationService.markedLocallyAsManuallyVerified() is called. + * + */ + fun markedAsManuallyVerified(userId: String, deviceId: String) {} } companion object { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt index fb94d61c0b..d131960893 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/Event.kt @@ -157,6 +157,11 @@ data class Event( */ fun isRedacted() = unsignedData?.redactedEvent != null + /** + * Tells if the event is redacted by the user himself. + */ + fun isRedactedBySameUser() = senderId == unsignedData?.redactedEvent?.senderId + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt index 06b3e9bf2e..3be9bdb7cc 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/RelationType.kt @@ -19,11 +19,12 @@ package im.vector.matrix.android.api.session.events.model * Constants defining known event relation types from Matrix specifications */ object RelationType { - /** Lets you define an event which annotates an existing event.*/ const val ANNOTATION = "m.annotation" /** Lets you define an event which replaces an existing event.*/ const val REPLACE = "m.replace" /** Lets you define an event which references an existing event.*/ const val REFERENCE = "m.reference" + /** Lets you define an event which adds a response to an existing event.*/ + const val RESPONSE = "m.response" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt index 129bfa3011..e87773db38 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/pushers/PushersService.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.api.session.pushers import androidx.lifecycle.LiveData import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.util.Cancelable import java.util.UUID interface PushersService { @@ -28,19 +29,32 @@ interface PushersService { /** * Add a new HTTP pusher. + * Note that only `http` kind is supported by the SDK for now. + * Ref: https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-pushers-set * - * @param pushkey the pushkey + * @param pushkey This is a unique identifier for this pusher. The value you should use for + * this is the routing or destination address information for the notification, + * for example, the APNS token for APNS or the Registration ID for GCM. If your + * notification client has no such concept, use any unique identifier. Max length, 512 chars. + * If the kind is "email", this is the email address to send notifications to. * @param appId the application id - * @param profileTag the profile tag - * @param lang the language - * @param appDisplayName a human-readable application name - * @param deviceDisplayName a human-readable device name - * @param url the URL that should be used to send notifications - * @param append append the pusher - * @param withEventIdOnly true to limit the push content + * This is a reverse-DNS style identifier for the application. It is recommended + * that this end with the platform, such that different platform versions get + * different app identifiers. Max length, 64 chars. + * @param profileTag This string determines which set of device specific rules this pusher executes. + * @param lang The preferred language for receiving notifications (e.g. "en" or "en-US"). + * @param appDisplayName A human readable string that will allow the user to identify what application owns this pusher. + * @param deviceDisplayName A human readable string that will allow the user to identify what device owns this pusher. + * @param url The URL to use to send notifications to. MUST be an HTTPS URL with a path of /_matrix/push/v1/notify. + * @param append If true, the homeserver should add another pusher with the given pushkey and App ID in addition + * to any others with different user IDs. Otherwise, the homeserver must remove any other pushers + * with the same App ID and pushkey for different users. + * @param withEventIdOnly true to limit the push content to only id and not message content + * Ref: https://matrix.org/docs/spec/push_gateway/r0.1.1#homeserver-behaviour * - * @return A work request uuid. Can be used to listen to the status - * (LiveData status = workManager.getWorkInfoByIdLiveData()) + * @return A work request uuid. Can be used to listen to the status + * (LiveData status = workManager.getWorkInfoByIdLiveData()) + * @throws [InvalidParameterException] if a parameter is not correct */ fun addHttpPusher(pushkey: String, appId: String, @@ -52,13 +66,18 @@ interface PushersService { append: Boolean, withEventIdOnly: Boolean): UUID - fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback) - - companion object { - const val EVENT_ID_ONLY = "event_id_only" - } + /** + * Remove the http pusher + */ + fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable + /** + * Get the current pushers, as a LiveData + */ fun getPushersLive(): LiveData> - fun pushers() : List + /** + * Get the current pushers + */ + fun getPushers(): List } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt index 28edfcfe04..89fca26aef 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/EventAnnotationsSummary.kt @@ -19,5 +19,6 @@ data class EventAnnotationsSummary( var eventId: String, var reactionsSummary: List, var editSummary: EditAggregatedSummary?, + var pollResponseSummary: PollResponseAggregatedSummary?, var referencesAggregatedSummary: ReferencesAggregatedSummary? = null ) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollResponseAggregatedSummary.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollResponseAggregatedSummary.kt new file mode 100644 index 0000000000..3256e5d7e1 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollResponseAggregatedSummary.kt @@ -0,0 +1,29 @@ +/* + * Copyright 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 + +data class PollResponseAggregatedSummary( + + var aggregatedContent: PollSummaryContent? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + val sourceEvents: List, + val localEchos: List +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollSummaryContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollSummaryContent.kt new file mode 100644 index 0000000000..2e6e3ed349 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/PollSummaryContent.kt @@ -0,0 +1,48 @@ +/* + * Copyright 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 + +import com.squareup.moshi.JsonClass + +/** + * Contains an aggregated summary info of the poll response. + * Put pre-computed info that you want to access quickly without having + * to go through all references events + */ +@JsonClass(generateAdapter = true) +data class PollSummaryContent( + // Index of my vote + var myVote: Int? = null, + // Array of VoteInfo, list is constructed so that there is only one vote by user + // And that optionIndex is valid + var votes: List? = null +) { + + fun voteCount(): Int { + return votes?.size ?: 0 + } + + fun voteCountForOption(optionIndex: Int) : Int { + return votes?.filter { it.optionIndex == optionIndex }?.count() ?: 0 + } +} + +@JsonClass(generateAdapter = true) +data class VoteInfo( + val userId: String, + val optionIndex: Int, + val voteTimestamp: Long +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt new file mode 100644 index 0000000000..33b133afaf --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageOptionsContent.kt @@ -0,0 +1,40 @@ +/* + * Copyright 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 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent + +// Possible values for optionType +const val OPTION_TYPE_POLL = "org.matrix.poll" +const val OPTION_TYPE_BUTTONS = "org.matrix.buttons" + +/** + * Polls and bot buttons are m.room.message events with a msgtype of m.options, + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessageOptionsContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_OPTIONS, + @Json(name = "type") val optionType: String? = null, + @Json(name = "body") override val body: String, + @Json(name = "label") val label: String?, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "options") val options: List? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessagePollResponseContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessagePollResponseContent.kt new file mode 100644 index 0000000000..dfd8059b9a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessagePollResponseContent.kt @@ -0,0 +1,33 @@ +/* + * 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 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import im.vector.matrix.android.api.session.events.model.Content +import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultContent + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class MessagePollResponseContent( + @Json(name = "msgtype") override val msgType: String = MessageType.MSGTYPE_RESPONSE, + @Json(name = "body") override val body: String, + @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, + @Json(name = "m.new_content") override val newContent: Content? = null +) : MessageContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt index 2707283325..c244500c9e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/MessageType.kt @@ -25,6 +25,9 @@ object MessageType { const val MSGTYPE_VIDEO = "m.video" const val MSGTYPE_LOCATION = "m.location" const val MSGTYPE_FILE = "m.file" + const val MSGTYPE_OPTIONS = "org.matrix.options" + const val MSGTYPE_RESPONSE = "org.matrix.response" + const val MSGTYPE_POLL_CLOSED = "org.matrix.poll_closed" const val MSGTYPE_VERIFICATION_REQUEST = "m.key.verification.request" // Add, in local, a fake message type in order to StickerMessage can inherit Message class // Because sticker isn't a message type but a event type without msgtype field diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/OptionItem.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/OptionItem.kt new file mode 100644 index 0000000000..0ea9e246ba --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/message/OptionItem.kt @@ -0,0 +1,29 @@ +/* + * 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 + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Ref: https://github.com/matrix-org/matrix-doc/pull/2192 + */ +@JsonClass(generateAdapter = true) +data class OptionItem( + @Json(name = "label") val label: String?, + @Json(name = "value") val value: String? +) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt index c4cbde98eb..622250da4e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/ReactionInfo.kt @@ -25,5 +25,6 @@ data class ReactionInfo( @Json(name = "event_id") override val eventId: String, val key: String, // always null for reaction - @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null ) : RelationContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt index c66d1b9770..d43c9f6a0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationContent.kt @@ -23,4 +23,5 @@ interface RelationContent { val type: String? val eventId: String? val inReplyTo: ReplyToContent? + val option: Int? } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt index 853a381740..892fc61dee 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/model/relation/RelationDefaultContent.kt @@ -22,5 +22,6 @@ import com.squareup.moshi.JsonClass data class RelationDefaultContent( @Json(name = "rel_type") override val type: String?, @Json(name = "event_id") override val eventId: String?, - @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null + @Json(name = "m.in_reply_to") override val inReplyTo: ReplyToContent? = null, + @Json(name = "option") override val option: Int? = null ) : RelationContent diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt index ac1b50bbcb..e6c32193f4 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/send/SendService.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room.send import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.util.Cancelable @@ -62,7 +63,24 @@ interface SendService { fun sendMedias(attachments: List): Cancelable /** - * Redacts (delete) the given event. + * Send a poll to the room. + * @param question the question + * @param options list of (label, value) + * @return a [Cancelable] + */ + fun sendPoll(question: String, options: List): Cancelable + + /** + * Method to send a poll response. + * @param pollEventId the poll currently replied to + * @param optionIndex The reply index + * @param optionValue The option value (for compatibility) + * @return a [Cancelable] + */ + fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable + + /** + * Redact (delete) the given event. * @param event The event to redact * @param reason Optional reason string */ diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt index 782bd63408..28375a091b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/CryptoModule.kt @@ -154,97 +154,95 @@ internal abstract class CryptoModule { } @Binds - abstract fun bindCryptoService(cryptoService: DefaultCryptoService): CryptoService + abstract fun bindCryptoService(service: DefaultCryptoService): CryptoService @Binds - abstract fun bindDeleteDeviceTask(deleteDeviceTask: DefaultDeleteDeviceTask): DeleteDeviceTask + abstract fun bindDeleteDeviceTask(task: DefaultDeleteDeviceTask): DeleteDeviceTask @Binds - abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask + abstract fun bindGetDevicesTask(task: DefaultGetDevicesTask): GetDevicesTask @Binds abstract fun bindGetDeviceInfoTask(task: DefaultGetDeviceInfoTask): GetDeviceInfoTask @Binds - abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask + abstract fun bindSetDeviceNameTask(task: DefaultSetDeviceNameTask): SetDeviceNameTask @Binds - abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask + abstract fun bindUploadKeysTask(task: DefaultUploadKeysTask): UploadKeysTask @Binds - abstract fun bindUploadSigningKeysTask(uploadKeysTask: DefaultUploadSigningKeysTask): UploadSigningKeysTask + abstract fun bindUploadSigningKeysTask(task: DefaultUploadSigningKeysTask): UploadSigningKeysTask @Binds - abstract fun bindUploadSignaturesTask(uploadSignaturesTask: DefaultUploadSignaturesTask): UploadSignaturesTask + abstract fun bindUploadSignaturesTask(task: DefaultUploadSignaturesTask): UploadSignaturesTask @Binds - abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask + abstract fun bindDownloadKeysForUsersTask(task: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask @Binds - abstract fun bindCreateKeysBackupVersionTask(createKeysBackupVersionTask: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask + abstract fun bindCreateKeysBackupVersionTask(task: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask @Binds - abstract fun bindDeleteBackupTask(deleteBackupTask: DefaultDeleteBackupTask): DeleteBackupTask + abstract fun bindDeleteBackupTask(task: DefaultDeleteBackupTask): DeleteBackupTask @Binds - abstract fun bindDeleteRoomSessionDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask + abstract fun bindDeleteRoomSessionDataTask(task: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask @Binds - abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionsDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask + abstract fun bindDeleteRoomSessionsDataTask(task: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask @Binds - abstract fun bindDeleteSessionsDataTask(deleteSessionsDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask + abstract fun bindDeleteSessionsDataTask(task: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask @Binds - abstract fun bindGetKeysBackupLastVersionTask(getKeysBackupLastVersionTask: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask + abstract fun bindGetKeysBackupLastVersionTask(task: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask @Binds - abstract fun bindGetKeysBackupVersionTask(getKeysBackupVersionTask: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask + abstract fun bindGetKeysBackupVersionTask(task: DefaultGetKeysBackupVersionTask): GetKeysBackupVersionTask @Binds - abstract fun bindGetRoomSessionDataTask(getRoomSessionDataTask: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask + abstract fun bindGetRoomSessionDataTask(task: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask @Binds - abstract fun bindGetRoomSessionsDataTask(getRoomSessionsDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask + abstract fun bindGetRoomSessionsDataTask(task: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask @Binds - abstract fun bindGetSessionsDataTask(getSessionsDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask + abstract fun bindGetSessionsDataTask(task: DefaultGetSessionsDataTask): GetSessionsDataTask @Binds - abstract fun bindStoreRoomSessionDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask + abstract fun bindStoreRoomSessionDataTask(task: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask @Binds - abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionsDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask + abstract fun bindStoreRoomSessionsDataTask(task: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask @Binds - abstract fun bindStoreSessionsDataTask(storeSessionsDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask + abstract fun bindStoreSessionsDataTask(task: DefaultStoreSessionsDataTask): StoreSessionsDataTask @Binds - abstract fun bindUpdateKeysBackupVersionTask(updateKeysBackupVersionTask: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask + abstract fun bindUpdateKeysBackupVersionTask(task: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask @Binds - abstract fun bindSendToDeviceTask(sendToDeviceTask: DefaultSendToDeviceTask): SendToDeviceTask + abstract fun bindSendToDeviceTask(task: DefaultSendToDeviceTask): SendToDeviceTask @Binds - abstract fun bindEncryptEventTask(encryptEventTask: DefaultEncryptEventTask): EncryptEventTask + abstract fun bindEncryptEventTask(task: DefaultEncryptEventTask): EncryptEventTask @Binds - abstract fun bindSendVerificationMessageTask(sendDefaultSendVerificationMessageTask: DefaultSendVerificationMessageTask): SendVerificationMessageTask + abstract fun bindSendVerificationMessageTask(task: DefaultSendVerificationMessageTask): SendVerificationMessageTask @Binds - abstract fun bindClaimOneTimeKeysForUsersDeviceTask(claimOneTimeKeysForUsersDevice: DefaultClaimOneTimeKeysForUsersDevice) - : ClaimOneTimeKeysForUsersDeviceTask + abstract fun bindClaimOneTimeKeysForUsersDeviceTask(task: DefaultClaimOneTimeKeysForUsersDevice): ClaimOneTimeKeysForUsersDeviceTask @Binds - abstract fun bindDeleteDeviceWithUserPasswordTask(deleteDeviceWithUserPasswordTask: DefaultDeleteDeviceWithUserPasswordTask) - : DeleteDeviceWithUserPasswordTask + abstract fun bindDeleteDeviceWithUserPasswordTask(task: DefaultDeleteDeviceWithUserPasswordTask): DeleteDeviceWithUserPasswordTask @Binds - abstract fun bindCrossSigningService(crossSigningService: DefaultCrossSigningService): CrossSigningService + abstract fun bindCrossSigningService(service: DefaultCrossSigningService): CrossSigningService @Binds - abstract fun bindCryptoStore(realmCryptoStore: RealmCryptoStore): IMXCryptoStore + abstract fun bindCryptoStore(store: RealmCryptoStore): IMXCryptoStore @Binds - abstract fun bindComputeShieldTrustTask(defaultShieldTrustUpdater: DefaultComputeTrustTask): ComputeTrustTask + abstract fun bindComputeShieldTrustTask(task: DefaultComputeTrustTask): ComputeTrustTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt index 920e5e9e4d..7fc3c0a549 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/crosssigning/DefaultCrossSigningService.kt @@ -637,9 +637,9 @@ internal class DefaultCrossSigningService @Inject constructor( // In this case it will change my MSK trust, and should then re-trigger a check of all other user trust setUserKeysAsTrusted(otherUserId, checkSelfTrust().isVerified()) } - - eventBus.post(CryptoToSessionUserTrustChange(userIds)) } + + eventBus.post(CryptoToSessionUserTrustChange(userIds)) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index fc14b0a23a..00ac4a6986 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -176,9 +176,9 @@ internal class DefaultVerificationService @Inject constructor( } } - private var listeners = ArrayList() + private var listeners = ArrayList() - override fun addListener(listener: VerificationService.VerificationListener) { + override fun addListener(listener: VerificationService.Listener) { uiHandler.post { if (!listeners.contains(listener)) { listeners.add(listener) @@ -186,7 +186,7 @@ internal class DefaultVerificationService @Inject constructor( } } - override fun removeListener(listener: VerificationService.VerificationListener) { + override fun removeListener(listener: VerificationService.Listener) { uiHandler.post { listeners.remove(listener) } @@ -1151,9 +1151,9 @@ internal class DefaultVerificationService @Inject constructor( return verificationRequest } - override fun declineVerificationRequestInDMs(otherUserId: String, otherDeviceId: String, transactionId: String, roomId: String) { + override fun declineVerificationRequestInDMs(otherUserId: String, transactionId: String, roomId: String) { verificationTransportRoomMessageFactory.createTransport(roomId, null) - .cancelTransaction(transactionId, otherUserId, otherDeviceId, CancelCode.User) + .cancelTransaction(transactionId, otherUserId, null, CancelCode.User) getExistingVerificationRequest(otherUserId, transactionId)?.let { updatePendingRequest(it.copy( diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt index 3379ddd2ed..fe5f9dadb9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/PendingVerificationRequest.kt @@ -15,8 +15,8 @@ */ package im.vector.matrix.android.internal.crypto.verification +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.crypto.sas.CancelCode -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SCAN import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_QR_CODE_SHOW import im.vector.matrix.android.internal.crypto.model.rest.VERIFICATION_METHOD_SAS @@ -46,11 +46,37 @@ data class PendingVerificationRequest( val isFinished: Boolean = isSuccessful || cancelConclusion != null - fun hasMethod(method: VerificationMethod): Boolean? { - return when (method) { - VerificationMethod.SAS -> readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS) - VerificationMethod.QR_CODE_SHOW -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW) - VerificationMethod.QR_CODE_SCAN -> readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN) + /** + * SAS is supported if I support it and the other party support it + */ + fun isSasSupported(): Boolean { + return requestInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_SAS).orFalse() + } + + /** + * Other can show QR code if I can scan QR code and other can show QR code + */ + fun otherCanShowQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } + } + + /** + * Other can scan QR code if I can show QR code and other can scan QR code + */ + fun otherCanScanQrCode(): Boolean { + return if (isIncoming) { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + } else { + requestInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SHOW).orFalse() + && readyInfo?.methods?.contains(VERIFICATION_METHOD_QR_CODE_SCAN).orFalse() } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt index bc85cddf26..ee0e66959d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransport.kt @@ -42,7 +42,7 @@ internal interface VerificationTransport { fun cancelTransaction(transactionId: String, otherUserId: String, - otherUserDeviceId: String, + otherUserDeviceId: String?, code: CancelCode) fun done(transactionId: String) @@ -79,11 +79,13 @@ internal interface VerificationTransport { fun createMac(tid: String, mac: Map, keys: String): VerificationInfoMac - fun createReady(tid: String, fromDevice: String, methods: List): VerificationInfoReady + fun createReady(tid: String, + fromDevice: String, + methods: List): VerificationInfoReady // TODO Refactor fun sendVerificationReady(keyReq: VerificationInfoReady, otherUserId: String, - otherDeviceId: String, + otherDeviceId: String?, callback: (() -> Unit)?) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt index 30d94c52d9..11093ca3ba 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportRoomMessage.kt @@ -208,7 +208,7 @@ internal class VerificationTransportRoomMessage( } } - override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String, code: CancelCode) { + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { Timber.d("## SAS canceling transaction $transactionId for reason $code") val event = createEventAndLocalEcho( type = EventType.KEY_VERIFICATION_CANCEL, @@ -337,7 +337,7 @@ internal class VerificationTransportRoomMessage( override fun sendVerificationReady(keyReq: VerificationInfoReady, otherUserId: String, - otherDeviceId: String, + otherDeviceId: String?, callback: (() -> Unit)?) { // Not applicable (send event is called directly) Timber.w("## SAS ignored verification ready with methods: ${keyReq.methods}") diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt index f76f8331bc..1dae8fba68 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationTransportToDevice.kt @@ -80,7 +80,7 @@ internal class VerificationTransportToDevice( override fun sendVerificationReady(keyReq: VerificationInfoReady, otherUserId: String, - otherDeviceId: String, + otherDeviceId: String?, callback: (() -> Unit)?) { Timber.d("## SAS sending verification ready with methods: ${keyReq.methods}") val contentMap = MXUsersDevicesMap() @@ -159,7 +159,7 @@ internal class VerificationTransportToDevice( .executeBy(taskExecutor) } - override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String, code: CancelCode) { + override fun cancelTransaction(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { Timber.d("## SAS canceling transaction $transactionId for reason $code") val cancelMessage = KeyVerificationCancel.create(transactionId, code) val contentMap = MXUsersDevicesMap() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt index ccdb8fb91f..ea0e34bf42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/EventAnnotationsSummaryMapper.kt @@ -55,7 +55,11 @@ internal object EventAnnotationsSummaryMapper { it.sourceEvents.toList(), it.sourceLocalEcho.toList() ) + }, + pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) } + ) } @@ -93,6 +97,9 @@ internal object EventAnnotationsSummaryMapper { RealmList().apply { addAll(it.localEchos) } ) } + eventAnnotationsSummaryEntity.pollResponseSummary = annotationsSummary.pollResponseSummary?.let { + PollResponseAggregatedSummaryEntityMapper.map(it) + } return eventAnnotationsSummaryEntity } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt new file mode 100644 index 0000000000..1d34c660bc --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/mapper/PollResponseAggregatedSummaryEntityMapper.kt @@ -0,0 +1,50 @@ +/* + * Copyright 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.database.mapper + +import im.vector.matrix.android.api.session.events.model.toContent +import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.PollResponseAggregatedSummary +import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity +import io.realm.RealmList + +internal object PollResponseAggregatedSummaryEntityMapper { + + fun map(entity: PollResponseAggregatedSummaryEntity): PollResponseAggregatedSummary { + return PollResponseAggregatedSummary( + aggregatedContent = ContentMapper.map(entity.aggregatedContent).toModel(), + closedTime = entity.closedTime, + localEchos = entity.sourceLocalEchoEvents.toList(), + sourceEvents = entity.sourceEvents.toList(), + nbOptions = entity.nbOptions + ) + } + + fun map(model: PollResponseAggregatedSummary): PollResponseAggregatedSummaryEntity { + return PollResponseAggregatedSummaryEntity( + aggregatedContent = ContentMapper.map(model.aggregatedContent.toContent()), + nbOptions = model.nbOptions, + closedTime = model.closedTime, + sourceEvents = RealmList().apply { addAll(model.sourceEvents) }, + sourceLocalEchoEvents = RealmList().apply { addAll(model.localEchos) } + ) + } +} + +internal fun PollResponseAggregatedSummaryEntity.asDomain(): PollResponseAggregatedSummary { + return PollResponseAggregatedSummaryEntityMapper.map(this) +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt index 1a4f72f0b1..8913f76b99 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/EventAnnotationsSummaryEntity.kt @@ -25,7 +25,8 @@ internal open class EventAnnotationsSummaryEntity( var roomId: String? = null, var reactionsSummary: RealmList = RealmList(), var editSummary: EditAggregatedSummaryEntity? = null, - var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null + var referencesSummaryEntity: ReferencesAggregatedSummaryEntity? = null, + var pollResponseSummary: PollResponseAggregatedSummaryEntity? = null ) : RealmObject() { companion object diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PollResponseAggregatedSummaryEntity.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PollResponseAggregatedSummaryEntity.kt new file mode 100644 index 0000000000..b17fbf07fa --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/PollResponseAggregatedSummaryEntity.kt @@ -0,0 +1,40 @@ +/* + * Copyright 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.database.model + +import io.realm.RealmList +import io.realm.RealmObject + +/** + * Keep the latest state of a poll + */ +internal open class PollResponseAggregatedSummaryEntity( + // For now we persist this a JSON for greater flexibility + // #see PollSummaryContent + var aggregatedContent: String? = null, + + // If set the poll is closed (Clients SHOULD NOT consider responses after the close event) + var closedTime: Long? = null, + // Clients SHOULD validate that the option in the relationship is a valid option, and ignore the response if invalid + var nbOptions: Int = 0, + + // The list of the eventIDs used to build the summary (might be out of sync if chunked received from message chunk) + var sourceEvents: RealmList = RealmList(), + var sourceLocalEchoEvents: RealmList = RealmList() +) : RealmObject() { + + companion object +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt index 298e887f0f..74768f8797 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/model/SessionRealmModule.kt @@ -40,6 +40,7 @@ import io.realm.annotations.RealmModule EventAnnotationsSummaryEntity::class, ReactionAggregatedSummaryEntity::class, EditAggregatedSummaryEntity::class, + PollResponseAggregatedSummaryEntity::class, ReferencesAggregatedSummaryEntity::class, PushRulesEntity::class, PushRuleEntity::class, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt index be7075ddd0..6e89a28b7d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/FilterContent.kt @@ -19,4 +19,5 @@ package im.vector.matrix.android.internal.database.query internal object FilterContent { internal const val EDIT_TYPE = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" + internal const val RESPONSE_TYPE = """{*"m.relates_to"*"rel_type":*"m.response"*}""" } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt index 8d9d305eec..5168d0728e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/TimelineEventEntityQueries.kt @@ -17,8 +17,15 @@ package im.vector.matrix.android.internal.database.query import im.vector.matrix.android.api.session.room.send.SendState -import im.vector.matrix.android.internal.database.model.* -import io.realm.* +import im.vector.matrix.android.internal.database.model.ChunkEntity +import im.vector.matrix.android.internal.database.model.RoomEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntity +import im.vector.matrix.android.internal.database.model.TimelineEventEntityFields +import io.realm.Realm +import io.realm.RealmList +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.Sort import io.realm.kotlin.where internal fun TimelineEventEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery { @@ -48,10 +55,16 @@ internal fun TimelineEventEntity.Companion.findWithSenderMembershipEvent(realm: internal fun TimelineEventEntity.Companion.latestEvent(realm: Realm, roomId: String, includesSending: Boolean, + filterContentRelation: Boolean = false, filterTypes: List = 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) + if (filterContentRelation) { + liveEvents + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) + ?.not()?.like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) + } val query = if (includesSending && sendingTimelineEvents.findAll().isNotEmpty()) { sendingTimelineEvents } else { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt index 98cf9e234e..c19c686329 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/MoshiProvider.kt @@ -47,6 +47,8 @@ object MoshiProvider { .registerSubtype(MessageLocationContent::class.java, MessageType.MSGTYPE_LOCATION) .registerSubtype(MessageFileContent::class.java, MessageType.MSGTYPE_FILE) .registerSubtype(MessageVerificationRequestContent::class.java, MessageType.MSGTYPE_VERIFICATION_REQUEST) + .registerSubtype(MessageOptionsContent::class.java, MessageType.MSGTYPE_OPTIONS) + .registerSubtype(MessagePollResponseContent::class.java, MessageType.MSGTYPE_RESPONSE) ) .add(SerializeNulls.JSON_ADAPTER_FACTORY) .build() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt index a418109cec..a1feb3e176 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/cache/CacheModule.kt @@ -37,5 +37,5 @@ internal abstract class CacheModule { } @Binds - abstract fun bindCacheService(cacheService: DefaultCacheService): CacheService + abstract fun bindCacheService(service: DefaultCacheService): CacheService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt index 7b787048c5..ce1a58d1b7 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/filter/FilterModule.kt @@ -37,11 +37,11 @@ internal abstract class FilterModule { } @Binds - abstract fun bindFilterRepository(filterRepository: DefaultFilterRepository): FilterRepository + abstract fun bindFilterRepository(repository: DefaultFilterRepository): FilterRepository @Binds - abstract fun bindFilterService(filterService: DefaultFilterService): FilterService + abstract fun bindFilterService(service: DefaultFilterService): FilterService @Binds - abstract fun bindSaveFilterTask(saveFilterTask: DefaultSaveFilterTask): SaveFilterTask + abstract fun bindSaveFilterTask(task: DefaultSaveFilterTask): SaveFilterTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt index 3b88dc5b6b..b48c6a96e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/group/GroupModule.kt @@ -37,8 +37,8 @@ internal abstract class GroupModule { } @Binds - abstract fun bindGetGroupDataTask(getGroupDataTask: DefaultGetGroupDataTask): GetGroupDataTask + abstract fun bindGetGroupDataTask(task: DefaultGetGroupDataTask): GetGroupDataTask @Binds - abstract fun bindGroupService(groupService: DefaultGroupService): GroupService + abstract fun bindGroupService(service: DefaultGroupService): GroupService } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt index 9082fceb9e..f2b249ab0c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/homeserver/HomeServerCapabilitiesModule.kt @@ -36,5 +36,5 @@ internal abstract class HomeServerCapabilitiesModule { } @Binds - abstract fun bindGetHomeServerCapabilitiesTask(getHomeServerCapabilitiesTask: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask + abstract fun bindGetHomeServerCapabilitiesTask(task: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPushersService.kt similarity index 71% rename from matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt rename to matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPushersService.kt index 9d80223149..12a995fbb9 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPusherService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/DefaultPushersService.kt @@ -21,6 +21,7 @@ import com.zhuinden.monarchy.Monarchy import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.session.pushers.Pusher import im.vector.matrix.android.api.session.pushers.PushersService +import im.vector.matrix.android.api.util.Cancelable import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.model.PusherEntity import im.vector.matrix.android.internal.database.query.where @@ -29,11 +30,12 @@ import im.vector.matrix.android.internal.di.WorkManagerProvider import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.worker.WorkerParamsFactory -import java.util.* +import java.security.InvalidParameterException +import java.util.UUID import java.util.concurrent.TimeUnit import javax.inject.Inject -internal class DefaultPusherService @Inject constructor( +internal class DefaultPushersService @Inject constructor( private val workManagerProvider: WorkManagerProvider, private val monarchy: Monarchy, @SessionId private val sessionId: String, @@ -48,10 +50,21 @@ internal class DefaultPusherService @Inject constructor( .executeBy(taskExecutor) } - override fun addHttpPusher(pushkey: String, appId: String, profileTag: String, - lang: String, appDisplayName: String, deviceDisplayName: String, - url: String, append: Boolean, withEventIdOnly: Boolean) + override fun addHttpPusher(pushkey: String, + appId: String, + profileTag: String, + lang: String, + appDisplayName: String, + deviceDisplayName: String, + url: String, + append: Boolean, + withEventIdOnly: Boolean) : UUID { + // Do some parameter checks. It's ok to throw Exception, to inform developer of the problem + if (pushkey.length > 512) throw InvalidParameterException("pushkey should not exceed 512 chars") + if (appId.length > 64) throw InvalidParameterException("appId should not exceed 64 chars") + if ("/_matrix/push/v1/notify" !in url) throw InvalidParameterException("url should contain '/_matrix/push/v1/notify'") + val pusher = JsonPusher( pushKey = pushkey, kind = "http", @@ -60,7 +73,7 @@ internal class DefaultPusherService @Inject constructor( deviceDisplayName = deviceDisplayName, profileTag = profileTag, lang = lang, - data = JsonPusherData(url, if (withEventIdOnly) PushersService.EVENT_ID_ONLY else null), + data = JsonPusherData(url, EVENT_ID_ONLY.takeIf { withEventIdOnly }), append = append) val params = AddHttpPusherWorker.Params(sessionId, pusher) @@ -74,9 +87,9 @@ internal class DefaultPusherService @Inject constructor( return request.id } - override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback) { + override fun removeHttpPusher(pushkey: String, appId: String, callback: MatrixCallback): Cancelable { val params = RemovePusherTask.Params(pushkey, appId) - removePusherTask + return removePusherTask .configureWith(params) { this.callback = callback } @@ -91,7 +104,11 @@ internal class DefaultPusherService @Inject constructor( ) } - override fun pushers(): List { + override fun getPushers(): List { return monarchy.fetchAllCopiedSync { PusherEntity.where(it) }.map { it.asDomain() } } + + companion object { + const val EVENT_ID_ONLY = "event_id_only" + } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt index 1564363e1b..35ee90bf42 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/pushers/PushersModule.kt @@ -49,38 +49,38 @@ internal abstract class PushersModule { } @Binds - abstract fun bindPusherService(pusherService: DefaultPusherService): PushersService + abstract fun bindPusherService(service: DefaultPushersService): PushersService @Binds - abstract fun bindConditionResolver(conditionResolver: DefaultConditionResolver): ConditionResolver + abstract fun bindConditionResolver(resolver: DefaultConditionResolver): ConditionResolver @Binds - abstract fun bindGetPushersTask(getPushersTask: DefaultGetPushersTask): GetPushersTask + abstract fun bindGetPushersTask(task: DefaultGetPushersTask): GetPushersTask @Binds - abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask + abstract fun bindGetPushRulesTask(task: DefaultGetPushRulesTask): GetPushRulesTask @Binds - abstract fun bindSavePushRulesTask(savePushRulesTask: DefaultSavePushRulesTask): SavePushRulesTask + abstract fun bindSavePushRulesTask(task: DefaultSavePushRulesTask): SavePushRulesTask @Binds - abstract fun bindRemovePusherTask(removePusherTask: DefaultRemovePusherTask): RemovePusherTask + abstract fun bindRemovePusherTask(task: DefaultRemovePusherTask): RemovePusherTask @Binds - abstract fun bindUpdatePushRuleEnableStatusTask(updatePushRuleEnableStatusTask: DefaultUpdatePushRuleEnableStatusTask): UpdatePushRuleEnableStatusTask + abstract fun bindUpdatePushRuleEnableStatusTask(task: DefaultUpdatePushRuleEnableStatusTask): UpdatePushRuleEnableStatusTask @Binds - abstract fun bindAddPushRuleTask(addPushRuleTask: DefaultAddPushRuleTask): AddPushRuleTask + abstract fun bindAddPushRuleTask(task: DefaultAddPushRuleTask): AddPushRuleTask @Binds - abstract fun bindRemovePushRuleTask(removePushRuleTask: DefaultRemovePushRuleTask): RemovePushRuleTask + abstract fun bindRemovePushRuleTask(task: DefaultRemovePushRuleTask): RemovePushRuleTask @Binds - abstract fun bindSetRoomNotificationStateTask(setRoomNotificationStateTask: DefaultSetRoomNotificationStateTask): SetRoomNotificationStateTask + abstract fun bindSetRoomNotificationStateTask(task: DefaultSetRoomNotificationStateTask): SetRoomNotificationStateTask @Binds - abstract fun bindPushRuleService(pushRuleService: DefaultPushRuleService): PushRuleService + abstract fun bindPushRuleService(service: DefaultPushRuleService): PushRuleService @Binds - abstract fun bindProcessEventForPushTask(processEventForPushTask: DefaultProcessEventForPushTask): ProcessEventForPushTask + abstract fun bindProcessEventForPushTask(task: DefaultProcessEventForPushTask): ProcessEventForPushTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt index def7f2c2f7..bd0a2e1b2e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/EventRelationsAggregationTask.kt @@ -25,8 +25,11 @@ import im.vector.matrix.android.api.session.events.model.LocalEcho import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.toContent import im.vector.matrix.android.api.session.events.model.toModel +import im.vector.matrix.android.api.session.room.model.PollSummaryContent import im.vector.matrix.android.api.session.room.model.ReferencesAggregatedContent +import im.vector.matrix.android.api.session.room.model.VoteInfo import im.vector.matrix.android.api.session.room.model.message.MessageContent +import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent import im.vector.matrix.android.api.session.room.model.relation.ReactionContent import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult @@ -36,6 +39,7 @@ import im.vector.matrix.android.internal.database.mapper.EventMapper import im.vector.matrix.android.internal.database.model.EditAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.EventAnnotationsSummaryEntity import im.vector.matrix.android.internal.database.model.EventEntity +import im.vector.matrix.android.internal.database.model.PollResponseAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntity import im.vector.matrix.android.internal.database.model.ReactionAggregatedSummaryEntityFields import im.vector.matrix.android.internal.database.model.ReferencesAggregatedSummaryEntity @@ -123,6 +127,9 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( Timber.v("###REPLACE in room $roomId for event ${event.eventId}") // A replace! handleReplace(realm, event, content, roomId, isLocalEcho) + } else if (content?.relatesTo?.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, content, roomId, isLocalEcho) } } @@ -144,13 +151,20 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( EventType.ENCRYPTED -> { // Relation type is in clear val encryptedEventContent = event.content.toModel() - if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE) { + if (encryptedEventContent?.relatesTo?.type == RelationType.REPLACE + || encryptedEventContent?.relatesTo?.type == RelationType.RESPONSE + ) { // we need to decrypt if needed decryptIfNeeded(event) event.getClearContent().toModel()?.let { - Timber.v("###REPLACE in room $roomId for event ${event.eventId}") - // A replace! - handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + if (encryptedEventContent.relatesTo.type == RelationType.REPLACE) { + Timber.v("###REPLACE in room $roomId for event ${event.eventId}") + // A replace! + handleReplace(realm, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } else if (encryptedEventContent.relatesTo.type == RelationType.RESPONSE) { + Timber.v("###RESPONSE in room $roomId for event ${event.eventId}") + handleResponse(realm, userId, event, it, roomId, isLocalEcho, encryptedEventContent.relatesTo.eventId) + } } } else if (encryptedEventContent?.relatesTo?.type == RelationType.REFERENCE) { decryptIfNeeded(event) @@ -276,6 +290,94 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( } } + private fun handleResponse(realm: Realm, + userId: String, + event: Event, + content: MessageContent, + roomId: String, + isLocalEcho: Boolean, + relatedEventId: String? = null) { + val eventId = event.eventId ?: return + val senderId = event.senderId ?: return + val targetEventId = relatedEventId ?: content.relatesTo?.eventId ?: return + val eventTimestamp = event.originServerTs ?: return + + // ok, this is a poll response + var existing = EventAnnotationsSummaryEntity.where(realm, targetEventId).findFirst() + if (existing == null) { + Timber.v("## POLL creating new relation summary for $targetEventId") + existing = EventAnnotationsSummaryEntity.create(realm, roomId, targetEventId) + } + + // we have it + val existingPollSummary = existing.pollResponseSummary + ?: realm.createObject(PollResponseAggregatedSummaryEntity::class.java).also { + existing.pollResponseSummary = it + } + + val closedTime = existingPollSummary?.closedTime + if (closedTime != null && eventTimestamp > closedTime) { + Timber.v("## POLL is closed ignore event poll:$targetEventId, event :${event.eventId}") + return + } + + val sumModel = ContentMapper.map(existingPollSummary?.aggregatedContent).toModel() ?: PollSummaryContent() + + if (existingPollSummary!!.sourceEvents.contains(eventId)) { + // ignore this event, we already know it (??) + Timber.v("## POLL ignoring event for summary, it's known eventId:$eventId") + return + } + val txId = event.unsignedData?.transactionId + // is it a remote echo? + if (!isLocalEcho && existingPollSummary.sourceLocalEchoEvents.contains(txId)) { + // ok it has already been managed + Timber.v("## POLL Receiving remote echo of response eventId:$eventId") + existingPollSummary.sourceLocalEchoEvents.remove(txId) + existingPollSummary.sourceEvents.add(event.eventId) + return + } + + val responseContent = event.content.toModel() ?: return Unit.also { + Timber.d("## POLL Receiving malformed response eventId:$eventId content: ${event.content}") + } + + val optionIndex = responseContent.relatesTo?.option ?: return Unit.also { + Timber.d("## POLL Ignoring malformed response no option eventId:$eventId content: ${event.content}") + } + + val votes = sumModel.votes?.toMutableList() ?: ArrayList() + val existingVoteIndex = votes.indexOfFirst { it.userId == senderId } + if (existingVoteIndex != -1) { + // Is the vote newer? + val existingVote = votes[existingVoteIndex] + if (existingVote.voteTimestamp < eventTimestamp) { + // Take the new one + votes[existingVoteIndex] = VoteInfo(senderId, optionIndex, eventTimestamp) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } else { + Timber.v("## POLL Ignoring vote (older than known one) eventId:$eventId ") + } + } else { + votes.add(VoteInfo(senderId, optionIndex, eventTimestamp)) + if (userId == senderId) { + sumModel.myVote = optionIndex + } + Timber.v("## POLL adding vote $optionIndex for user $senderId in poll :$relatedEventId ") + } + sumModel.votes = votes + if (isLocalEcho) { + existingPollSummary.sourceLocalEchoEvents.add(eventId) + } else { + existingPollSummary.sourceEvents.add(eventId) + } + + existingPollSummary.aggregatedContent = ContentMapper.map(sumModel.toContent()) + } + private fun handleInitialAggregatedRelations(event: Event, roomId: String, aggregation: AggregatedAnnotation, realm: Realm) { if (SHOULD_HANDLE_SERVER_AGREGGATION) { aggregation.chunk?.forEach { @@ -459,7 +561,7 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor( EventType.KEY_VERIFICATION_ACCEPT -> { updateVerificationState(currentState, VerificationState.WAITING) } - EventType.KEY_VERIFICATION_READY -> { + EventType.KEY_VERIFICATION_READY -> { updateVerificationState(currentState, VerificationState.WAITING) } EventType.KEY_VERIFICATION_KEY -> { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt index fbfe2b403e..a441d17196 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomSummaryUpdater.kt @@ -102,7 +102,8 @@ internal class RoomSummaryUpdater @Inject constructor( roomSummaryEntity.membership = membership } - val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) + val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, + filterTypes = PREVIEWABLE_TYPES, filterContentRelation = true) val lastTopicEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_TOPIC, stateKey = "")?.root val lastCanonicalAliasEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_CANONICAL_ALIAS, stateKey = "")?.root diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt index 30247ade12..fbb80adf83 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/DefaultSendService.kt @@ -28,6 +28,7 @@ import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.isImageMessage import im.vector.matrix.android.api.session.events.model.isTextMessage +import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.timeline.TimelineEvent @@ -80,7 +81,20 @@ internal class DefaultSendService @AssistedInject constructor( val event = localEchoEventFactory.createFormattedTextEvent(roomId, TextContent(text, formattedText), msgType).also { createLocalEcho(it) } + return sendEvent(event) + } + override fun sendPoll(question: String, options: List): Cancelable { + val event = localEchoEventFactory.createPollEvent(roomId, question, options).also { + createLocalEcho(it) + } + return sendEvent(event) + } + + override fun sendOptionsReply(pollEventId: String, optionIndex: Int, optionValue: String): Cancelable { + val event = localEchoEventFactory.createOptionsReplyEvent(roomId, pollEventId, optionIndex, optionValue).also { + createLocalEcho(it) + } return sendEvent(event) } @@ -158,123 +172,123 @@ internal class DefaultSendService @AssistedInject constructor( } } - override fun clearSendingQueue() { - timelineSendEventWorkCommon.cancelAllWorks(roomId) - workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK)) + override fun clearSendingQueue() { + timelineSendEventWorkCommon.cancelAllWorks(roomId) + workManagerProvider.workManager.cancelUniqueWork(buildWorkName(UPLOAD_WORK)) - // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied - workManagerProvider.matrixOneTimeWorkRequestBuilder() - .build().let { - timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE) + // Replace the worker chains with a AlwaysSuccessfulWorker, to ensure the queues are well emptied + workManagerProvider.matrixOneTimeWorkRequestBuilder() + .build().let { + timelineSendEventWorkCommon.postWork(roomId, it, ExistingWorkPolicy.REPLACE) - // need to clear also image sending queue - workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) - .enqueue() - } - taskExecutor.executorScope.launch { - localEchoRepository.clearSendingQueue(roomId) - } - } - - override fun resendAllFailedMessages() { - taskExecutor.executorScope.launch { - val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) - eventsToResend.forEach { - sendEvent(it) + // need to clear also image sending queue + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.REPLACE, it) + .enqueue() } - localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) - } - } - - override fun sendMedia(attachment: ContentAttachmentData): Cancelable { - // Create an event with the media file path - val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { - createLocalEcho(it) - } - return internalSendMedia(event, attachment) - } - - private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable { - val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) - - val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) - val sendWork = createSendEventWork(localEcho, false) - - if (isRoomEncrypted) { - val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) - - val op: Operation = workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(encryptWork) - .then(sendWork) - .enqueue() - op.result.addListener(Runnable { - if (op.result.isCancelled) { - Timber.e("CHAIN WAS CANCELLED") - } else if (op.state.value is Operation.State.FAILURE) { - Timber.e("CHAIN DID FAIL") - } - }, workerFutureListenerExecutor) - } else { - workManagerProvider.workManager - .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) - .then(sendWork) - .enqueue() - } - - return CancelableWork(workManagerProvider.workManager, sendWork.id) - } - - private fun createLocalEcho(event: Event) { - localEchoEventFactory.createLocalEcho(event) - } - - private fun buildWorkName(identifier: String): String { - return "${roomId}_$identifier" - } - - private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - // Same parameter - val params = EncryptEventWorker.Params(sessionId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(params) - - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .setInputData(sendWorkData) - .startChain(startChain) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() - } - - private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { - val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) - val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) - - return timelineSendEventWorkCommon.createWork(sendWorkData, 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(redactWorkData, true) - } - - private fun createUploadMediaWork(event: Event, - attachment: ContentAttachmentData, - isRoomEncrypted: Boolean, - startChain: Boolean): OneTimeWorkRequest { - val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) - val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) - - return workManagerProvider.matrixOneTimeWorkRequestBuilder() - .setConstraints(WorkManagerProvider.workConstraints) - .startChain(startChain) - .setInputData(uploadWorkData) - .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) - .build() + taskExecutor.executorScope.launch { + localEchoRepository.clearSendingQueue(roomId) } } + + override fun resendAllFailedMessages() { + taskExecutor.executorScope.launch { + val eventsToResend = localEchoRepository.getAllFailedEventsToResend(roomId) + eventsToResend.forEach { + sendEvent(it) + } + localEchoRepository.updateSendState(roomId, eventsToResend.mapNotNull { it.eventId }, SendState.UNSENT) + } + } + + override fun sendMedia(attachment: ContentAttachmentData): Cancelable { + // Create an event with the media file path + val event = localEchoEventFactory.createMediaEvent(roomId, attachment).also { + createLocalEcho(it) + } + return internalSendMedia(event, attachment) + } + + private fun internalSendMedia(localEcho: Event, attachment: ContentAttachmentData): Cancelable { + val isRoomEncrypted = cryptoService.isRoomEncrypted(roomId) + + val uploadWork = createUploadMediaWork(localEcho, attachment, isRoomEncrypted, startChain = true) + val sendWork = createSendEventWork(localEcho, false) + + if (isRoomEncrypted) { + val encryptWork = createEncryptEventWork(localEcho, false /*not start of chain, take input error*/) + + val op: Operation = workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(encryptWork) + .then(sendWork) + .enqueue() + op.result.addListener(Runnable { + if (op.result.isCancelled) { + Timber.e("CHAIN WAS CANCELLED") + } else if (op.state.value is Operation.State.FAILURE) { + Timber.e("CHAIN DID FAIL") + } + }, workerFutureListenerExecutor) + } else { + workManagerProvider.workManager + .beginUniqueWork(buildWorkName(UPLOAD_WORK), ExistingWorkPolicy.APPEND, uploadWork) + .then(sendWork) + .enqueue() + } + + return CancelableWork(workManagerProvider.workManager, sendWork.id) + } + + private fun createLocalEcho(event: Event) { + localEchoEventFactory.createLocalEcho(event) + } + + private fun buildWorkName(identifier: String): String { + return "${roomId}_$identifier" + } + + private fun createEncryptEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + // Same parameter + val params = EncryptEventWorker.Params(sessionId, roomId, event) + val sendWorkData = WorkerParamsFactory.toData(params) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .setInputData(sendWorkData) + .startChain(startChain) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } + + private fun createSendEventWork(event: Event, startChain: Boolean): OneTimeWorkRequest { + val sendContentWorkerParams = SendEventWorker.Params(sessionId, roomId, event) + val sendWorkData = WorkerParamsFactory.toData(sendContentWorkerParams) + + return timelineSendEventWorkCommon.createWork(sendWorkData, 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(redactWorkData, true) + } + + private fun createUploadMediaWork(event: Event, + attachment: ContentAttachmentData, + isRoomEncrypted: Boolean, + startChain: Boolean): OneTimeWorkRequest { + val uploadMediaWorkerParams = UploadContentWorker.Params(sessionId, roomId, event, attachment, isRoomEncrypted) + val uploadWorkData = WorkerParamsFactory.toData(uploadMediaWorkerParams) + + return workManagerProvider.matrixOneTimeWorkRequestBuilder() + .setConstraints(WorkManagerProvider.workConstraints) + .startChain(startChain) + .setInputData(uploadWorkData) + .setBackoffCriteria(BackoffPolicy.LINEAR, BACKOFF_DELAY, TimeUnit.MILLISECONDS) + .build() + } +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt index 201e0b2322..21080d9037 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/EncryptEventWorker.kt @@ -76,9 +76,6 @@ internal class EncryptEventWorker(context: Context, params: WorkerParameters) localMutableContent.remove(it) } - crypto.downloadKeys(listOf("@testxsigningvfe:matrix.org"), true, object : MatrixCallback { - }) - var error: Throwable? = null var result: MXEncryptEventContentResult? = null try { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt index bfa1d380ae..f77f4b7f3a 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/send/LocalEchoEventFactory.kt @@ -36,10 +36,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageContent 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 +import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent +import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_POLL +import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.model.message.ThumbnailInfo import im.vector.matrix.android.api.session.room.model.message.VideoInfo import im.vector.matrix.android.api.session.room.model.message.isReply @@ -132,6 +136,43 @@ internal class LocalEchoEventFactory @Inject constructor( )) } + fun createOptionsReplyEvent(roomId: String, + pollEventId: String, + optionIndex: Int, + optionLabel: String): Event { + return createEvent(roomId, + MessagePollResponseContent( + body = optionLabel, + relatesTo = RelationDefaultContent( + type = RelationType.RESPONSE, + option = optionIndex, + eventId = pollEventId) + + )) + } + + fun createPollEvent(roomId: String, + question: String, + options: List): Event { + val compatLabel = buildString { + append("[Poll] ") + append(question) + options.forEach { + append("\n") + append(it.value) + } + } + return createEvent( + roomId, + MessageOptionsContent( + body = compatLabel, + label = question, + optionType = OPTION_TYPE_POLL, + options = options.toList() + ) + ) + } + fun createReplaceTextOfReply(roomId: String, eventReplaced: TimelineEvent, originalEvent: TimelineEvent, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index d3b0787b68..19b87122e8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -725,6 +725,7 @@ internal class DefaultTimeline( } if (settings.filterEdits) { not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.EDIT_TYPE) + not().like(TimelineEventEntityFields.ROOT.CONTENT, FilterContent.RESPONSE_TYPE) } return this } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt index 05912ba240..056f942211 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/TimelineHiddenReadReceipts.kt @@ -157,6 +157,8 @@ internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSu } if (settings.filterEdits) { like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.EDIT_TYPE) + or() + like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT}.${TimelineEventEntityFields.ROOT.CONTENT}", FilterContent.RESPONSE_TYPE) } endGroup() return this diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt index c9081d1466..2add3f4eb5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/sync/SyncModule.kt @@ -36,5 +36,5 @@ internal abstract class SyncModule { } @Binds - abstract fun bindSyncTask(syncTask: DefaultSyncTask): SyncTask + abstract fun bindSyncTask(task: DefaultSyncTask): SyncTask } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt index 22d012269b..fa5a51b4eb 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/user/UserModule.kt @@ -43,10 +43,10 @@ internal abstract class UserModule { } @Binds - abstract fun bindUserService(userService: DefaultUserService): UserService + abstract fun bindUserService(service: DefaultUserService): UserService @Binds - abstract fun bindSearchUserTask(searchUserTask: DefaultSearchUserTask): SearchUserTask + abstract fun bindSearchUserTask(task: DefaultSearchUserTask): SearchUserTask @Binds abstract fun bindSaveIgnoredUsersTask(task: DefaultSaveIgnoredUsersTask): SaveIgnoredUsersTask @@ -55,5 +55,5 @@ internal abstract class UserModule { abstract fun bindUpdateIgnoredUserIdsTask(task: DefaultUpdateIgnoredUserIdsTask): UpdateIgnoredUserIdsTask @Binds - abstract fun bindUserStore(userStore: RealmUserStore): UserStore + abstract fun bindUserStore(store: RealmUserStore): UserStore } diff --git a/tools/release/download_buildkite_artifacts.py b/tools/release/download_buildkite_artifacts.py index 849b98582e..ef4251a14f 100755 --- a/tools/release/download_buildkite_artifacts.py +++ b/tools/release/download_buildkite_artifacts.py @@ -44,7 +44,6 @@ parser.add_argument('-b', parser.add_argument('-e', '--expecting', type=int, - default=-1, help='the expected number of artifacts. If omitted, no check will be done.') parser.add_argument('-d', '--directory', @@ -93,8 +92,8 @@ print(" git commit message : \"%s\"" % data0.get('message')) print(" build state : %s" % data0.get('state')) if data0.get('state') != 'passed': - print("❌ Error, the build failed (state: %s)" % data0.get('state')) - exit(0) + print("❌ Error, the build is in state '%s', and not 'passed'" % data0.get('state')) + exit(1) ### Fetch artifacts list @@ -110,7 +109,7 @@ data = json.loads(r.content.decode()) print(" %d artifact(s) found." % len(data)) -if args.expecting != -1 and args.expecting != len(data): +if args.expecting is not None and args.expecting != len(data): print("Error, expecting %d artifacts and found %d." % (args.expecting, len(data))) exit(1) diff --git a/vector/build.gradle b/vector/build.gradle index fca82f935e..0517482904 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -15,7 +15,7 @@ androidExtensions { } ext.versionMajor = 0 -ext.versionMinor = 15 +ext.versionMinor = 16 ext.versionPatch = 0 static def getGitTimestamp() { diff --git a/vector/src/gplay/java/im/vector/riotx/gplay/features/settings/troubleshoot/TestTokenRegistration.kt b/vector/src/gplay/java/im/vector/riotx/gplay/features/settings/troubleshoot/TestTokenRegistration.kt index 5468217527..75e888dfb7 100644 --- a/vector/src/gplay/java/im/vector/riotx/gplay/features/settings/troubleshoot/TestTokenRegistration.kt +++ b/vector/src/gplay/java/im/vector/riotx/gplay/features/settings/troubleshoot/TestTokenRegistration.kt @@ -47,10 +47,10 @@ class TestTokenRegistration @Inject constructor(private val context: AppCompatAc status = TestStatus.FAILED return } - val pusher = session.pushers().filter { + val pushers = session.getPushers().filter { it.pushKey == fcmToken && it.state == PusherState.REGISTERED } - if (pusher.isEmpty()) { + if (pushers.isEmpty()) { description = stringProvider.getString(R.string.settings_troubleshoot_test_token_registration_failed, stringProvider.getString(R.string.sas_error_unknown)) quickFix = object : TroubleshootQuickFix(R.string.settings_troubleshoot_test_token_registration_quick_fix) { diff --git a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt index e2c08a1fe8..aa1fbaca54 100644 --- a/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt +++ b/vector/src/main/java/im/vector/riotx/core/pushers/PushersManager.kt @@ -47,8 +47,8 @@ class PushersManager @Inject constructor( appNameProvider.getAppName(), currentSession.sessionParams.credentials.deviceId ?: "MOBILE", stringProvider.getString(R.string.pusher_http_url), - false, - true + append = false, + withEventIdOnly = true ) } diff --git a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt index c714be7650..bc5a1aff95 100644 --- a/vector/src/main/java/im/vector/riotx/features/MainActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/MainActivity.kt @@ -85,7 +85,7 @@ class MainActivity : VectorBaseActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) args = parseArgs() - if (args.clearCredentials || args.isUserLoggedOut) { + if (args.clearCredentials || args.isUserLoggedOut || args.clearCache) { clearNotifications() } // Handle some wanted cleanup diff --git a/vector/src/main/java/im/vector/riotx/features/command/Command.kt b/vector/src/main/java/im/vector/riotx/features/command/Command.kt index 5c96b7c93c..f39f1cb7cd 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/Command.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/Command.kt @@ -41,6 +41,7 @@ enum class Command(val command: String, val parameters: String, @StringRes val d RAINBOW_EMOTE("/rainbowme", "", R.string.command_description_rainbow_emote), CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token), SPOILER("/spoiler", "", R.string.command_description_spoiler), + POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), SHRUG("/shrug", "", R.string.command_description_shrug), // TODO temporary command VERIFY_USER("/verify", "", R.string.command_description_verify); diff --git a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt index d4f5010d7e..abc047e273 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/CommandParser.kt @@ -80,12 +80,12 @@ object CommandParser { ParsedCommand.SendEmote(message) } - Command.RAINBOW.command -> { + Command.RAINBOW.command -> { val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim() ParsedCommand.SendRainbow(message) } - Command.RAINBOW_EMOTE.command -> { + Command.RAINBOW_EMOTE.command -> { val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim() ParsedCommand.SendRainbowEmote(message) @@ -251,7 +251,6 @@ object CommandParser { } Command.SPOILER.command -> { val message = textMessage.substring(Command.SPOILER.command.length).trim() - ParsedCommand.SendSpoiler(message) } Command.SHRUG.command -> { @@ -259,12 +258,20 @@ object CommandParser { ParsedCommand.SendShrug(message) } - Command.VERIFY_USER.command -> { val message = textMessage.substring(Command.VERIFY_USER.command.length).trim() ParsedCommand.VerifyUser(message) } + Command.POLL.command -> { + val rawCommand = textMessage.substring(Command.POLL.command.length).trim() + val split = rawCommand.split("|").map { it.trim() } + if (split.size > 2) { + ParsedCommand.SendPoll(split[0], split.subList(1, split.size)) + } else { + ParsedCommand.ErrorSyntax(Command.POLL) + } + } else -> { // Unknown command ParsedCommand.ErrorUnknownSlashCommand(slashCommand) diff --git a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt index dd9fe32e09..d823429ac9 100644 --- a/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt +++ b/vector/src/main/java/im/vector/riotx/features/command/ParsedCommand.kt @@ -50,4 +50,5 @@ sealed class ParsedCommand { class SendSpoiler(val message: String) : ParsedCommand() class SendShrug(val message: CharSequence) : ParsedCommand() class VerifyUser(val userId: String) : ParsedCommand() + class SendPoll(val question: String, val options: List) : ParsedCommand() } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt index 856c71f888..f890aef91b 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/keysrequest/KeyRequestHandler.kt @@ -56,7 +56,7 @@ import kotlin.collections.HashMap @Singleton class KeyRequestHandler @Inject constructor(private val context: Context) : RoomKeysRequestListener, - VerificationService.VerificationListener { + VerificationService.Listener { private val alertsToRequests = HashMap>() @@ -262,9 +262,6 @@ class KeyRequestHandler @Inject constructor(private val context: Context) } } - override fun transactionCreated(tx: VerificationTransaction) { - } - override fun transactionUpdated(tx: VerificationTransaction) { if (tx is SasVerificationTransaction) { val state = tx.state diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt index 0fbbccee8a..8765dbc0d9 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/IncomingVerificationRequestHandler.kt @@ -34,7 +34,7 @@ import javax.inject.Singleton * Listens to the VerificationManager and add a new notification when an incoming request is detected. */ @Singleton -class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.VerificationListener { +class IncomingVerificationRequestHandler @Inject constructor(private val context: Context) : VerificationService.Listener { private var session: Session? = null @@ -48,8 +48,6 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context this.session = null } - override fun transactionCreated(tx: VerificationTransaction) {} - override fun transactionUpdated(tx: VerificationTransaction) { if (!tx.isToDeviceTransport()) return // TODO maybe check also if @@ -111,9 +109,6 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context } } - override fun markedAsManuallyVerified(userId: String, deviceId: String) { - } - override fun verificationRequestCreated(pr: PendingVerificationRequest) { // For incoming request we should prompt (if not in activity where this request apply) if (pr.isIncoming) { @@ -145,7 +140,6 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context } dismissedAction = Runnable { session?.getVerificationService()?.declineVerificationRequestInDMs(pr.otherUserId, - pr.requestInfo?.fromDevice ?: "", pr.transactionId ?: "", pr.roomId ?: "" ) @@ -163,7 +157,6 @@ class IncomingVerificationRequestHandler @Inject constructor(private val context if (pr.isIncoming && (pr.isReady || pr.handledByOtherSession)) { PopupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) } - super.verificationRequestUpdated(pr) } private fun uniqueIdForVerificationRequest(pr: PendingVerificationRequest) = diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt index a9f9987c7f..85b878fe16 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/VerificationBottomSheetViewModel.kt @@ -60,7 +60,7 @@ class VerificationBottomSheetViewModel @AssistedInject constructor(@Assisted ini @Assisted args: VerificationBottomSheet.VerificationArgs, private val session: Session) : VectorViewModel(initialState), - VerificationService.VerificationListener { + VerificationService.Listener { init { session.getVerificationService().addListener(this) diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt index 75c1b69058..bdb07ed0dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/choose/VerificationChooseMethodViewModel.kt @@ -21,9 +21,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.AssistedInject +import im.vector.matrix.android.api.extensions.orFalse import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.crypto.sas.QrCodeVerificationTransaction -import im.vector.matrix.android.api.session.crypto.sas.VerificationMethod import im.vector.matrix.android.api.session.crypto.sas.VerificationService import im.vector.matrix.android.api.session.crypto.sas.VerificationTransaction import im.vector.matrix.android.internal.crypto.verification.PendingVerificationRequest @@ -45,7 +45,7 @@ data class VerificationChooseMethodViewState( class VerificationChooseMethodViewModel @AssistedInject constructor( @Assisted initialState: VerificationChooseMethodViewState, private val session: Session -) : VectorViewModel(initialState), VerificationService.VerificationListener { +) : VectorViewModel(initialState), VerificationService.Listener { override fun transactionCreated(tx: VerificationTransaction) { transactionUpdated(tx) @@ -66,9 +66,9 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( setState { copy( - otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false, - otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false, - SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false + otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), + otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), + SASModeAvailable = pvr?.isSasSupported().orFalse() ) } } @@ -103,10 +103,10 @@ class VerificationChooseMethodViewModel @AssistedInject constructor( return VerificationChooseMethodViewState(otherUserId = args.otherUserId, transactionId = args.verificationId ?: "", - otherCanShowQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SHOW) ?: false, - otherCanScanQrCode = pvr?.hasMethod(VerificationMethod.QR_CODE_SCAN) ?: false, + otherCanShowQrCode = pvr?.otherCanShowQrCode().orFalse(), + otherCanScanQrCode = pvr?.otherCanScanQrCode().orFalse(), qrCodeText = (qrCodeVerificationTransaction as? QrCodeVerificationTransaction)?.qrCodeText, - SASModeAvailable = pvr?.hasMethod(VerificationMethod.SAS) ?: false + SASModeAvailable = pvr?.isSasSupported().orFalse() ) } } diff --git a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt index 69d106a4b4..637b7d7cc9 100644 --- a/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/crypto/verification/emoji/VerificationEmojiCodeViewModel.kt @@ -52,7 +52,7 @@ data class VerificationEmojiCodeViewState( class VerificationEmojiCodeViewModel @AssistedInject constructor( @Assisted initialState: VerificationEmojiCodeViewState, private val session: Session -) : VectorViewModel(initialState), VerificationService.VerificationListener { +) : VectorViewModel(initialState), VerificationService.Listener { init { withState { state -> diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt index d0716bd047..3061c5c9e2 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailAction.kt @@ -53,6 +53,8 @@ sealed class RoomDetailAction : VectorViewModelAction { data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() + data class ReplyToOptions(val eventId: String, val optionIndex: Int, val optionValue: String) : RoomDetailAction() + data class ReportContent( val eventId: String, val senderId: String?, @@ -65,8 +67,8 @@ sealed class RoomDetailAction : VectorViewModelAction { object ClearSendQueue : RoomDetailAction() object ResendAll : RoomDetailAction() - data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() - data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction() + data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() + data class DeclineVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() data class RequestVerification(val userId: String) : RoomDetailAction() - data class ResumeVerification(val transactionId: String, val otherUserId: String? = null, val otherdDeviceId: String? = null) : RoomDetailAction() + data class ResumeVerification(val transactionId: String, val otherUserId: String?) : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 9e4d7d8163..d744c7f443 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -61,8 +61,10 @@ import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState import com.github.piasy.biv.BigImageViewer import com.github.piasy.biv.loader.ImageLoader +import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout import com.jakewharton.rxbinding3.widget.textChanges import im.vector.matrix.android.api.permalinks.PermalinkFactory import im.vector.matrix.android.api.session.Session @@ -786,6 +788,31 @@ class RoomDetailFragment @Inject constructor( .show() } + private fun promptConfirmationToRedactEvent(action: EventSharedAction.Redact) { + val layout = requireActivity().layoutInflater.inflate(R.layout.dialog_delete_event, null) + val reasonCheckBox = layout.findViewById(R.id.deleteEventReasonCheck) + val reasonTextInputLayout = layout.findViewById(R.id.deleteEventReasonTextInputLayout) + val reasonInput = layout.findViewById(R.id.deleteEventReasonInput) + + reasonCheckBox.isVisible = action.askForReason + reasonTextInputLayout.isVisible = action.askForReason + + reasonCheckBox.setOnCheckedChangeListener { _, isChecked -> reasonTextInputLayout.isEnabled = isChecked } + + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.delete_event_dialog_title) + .setView(layout) + .setPositiveButton(R.string.remove) { _, _ -> + val reason = reasonInput.text.toString() + .takeIf { action.askForReason } + ?.takeIf { reasonCheckBox.isChecked } + ?.takeIf { it.isNotBlank() } + roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, reason)) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + private fun displayRoomDetailActionFailure(result: RoomDetailViewEvents.ActionFailure) { AlertDialog.Builder(requireActivity()) .setTitle(R.string.dialog_title_error) @@ -1005,7 +1032,7 @@ class RoomDetailFragment @Inject constructor( override fun onEventCellClicked(informationData: MessageInformationData, messageContent: MessageContent?, view: View) { if (messageContent is MessageVerificationRequestContent) { - roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId)) + roomDetailViewModel.handle(RoomDetailAction.ResumeVerification(informationData.eventId, null)) } } @@ -1022,7 +1049,7 @@ class RoomDetailFragment @Inject constructor( } override fun onAvatarClicked(informationData: MessageInformationData) { - // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.senderId)) + // roomDetailViewModel.handle(RoomDetailAction.RequestVerification(informationData.userId)) openRoomMemberProfile(informationData.senderId) } @@ -1083,7 +1110,7 @@ class RoomDetailFragment @Inject constructor( private fun handleActions(action: EventSharedAction) { when (action) { is EventSharedAction.OpenUserProfile -> { - openRoomMemberProfile(action.senderId) + openRoomMemberProfile(action.userId) } is EventSharedAction.AddReaction -> { startActivityForResult(EmojiReactionPickerActivity.intent(requireContext(), action.eventId), REACTION_SELECT_REQUEST_CODE) @@ -1097,8 +1124,8 @@ class RoomDetailFragment @Inject constructor( copyToClipboard(requireContext(), action.content, false) showSnackWithMessage(getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT) } - is EventSharedAction.Delete -> { - roomDetailViewModel.handle(RoomDetailAction.RedactAction(action.eventId, context?.getString(R.string.event_redacted_by_user_reason))) + is EventSharedAction.Redact -> { + promptConfirmationToRedactEvent(action) } is EventSharedAction.Share -> { // TODO current data communication is too limited diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index 710c70a948..2ca372707f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -43,6 +43,7 @@ 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.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.MessageType +import im.vector.matrix.android.api.session.room.model.message.OptionItem import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent import im.vector.matrix.android.api.session.room.read.ReadService @@ -199,6 +200,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) is RoomDetailAction.RequestVerification -> handleRequestVerification(action) @@ -422,6 +424,11 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) popDraft() } + is ParsedCommand.SendPoll -> { + room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) + _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) + popDraft() + } is ParsedCommand.ChangeTopic -> { handleChangeTopicSlashCommand(slashCommandResult) popDraft() @@ -833,7 +840,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) { session.getVerificationService().declineVerificationRequestInDMs( action.otherUserId, - action.otherdDeviceId, action.transactionId, room.roomId) } @@ -855,6 +861,10 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro } } + private fun handleReplyToOptions(action: RoomDetailAction.ReplyToOptions) { + room.sendOptionsReply(action.eventId, action.optionIndex, action.optionValue) + } + private fun observeSyncState() { session.rx() .liveSyncState() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt index 6bc93f28dc..589be112dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/readreceipts/DisplayReadReceiptItem.kt @@ -33,6 +33,7 @@ abstract class DisplayReadReceiptItem : EpoxyModelWithHolder Unit)? = null override fun bind(holder: Holder) { avatarRenderer.render(matrixItem, holder.avatarView) @@ -43,6 +44,7 @@ abstract class DisplayReadReceiptItem : EpoxyModelWithHolder>() { + var listener: Listener? = null + override fun buildModels(readReceipts: List) { readReceipts.forEach { val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) @@ -40,7 +42,12 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte .matrixItem(it.toMatrixItem()) .avatarRenderer(avatarRender) .timestamp(timestamp) + .userClicked { listener?.didSelectUser(it.userId) } .addIf(session.myUserId != it.userId, this) } } + + interface Listener { + fun didSelectUser(userId: String) + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt index 91922611dd..cba89d8481 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/EventSharedAction.kt @@ -22,11 +22,13 @@ import im.vector.riotx.R import im.vector.riotx.core.platform.VectorSharedAction import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData -sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val iconResId: Int) : VectorSharedAction { +sealed class EventSharedAction(@StringRes val titleRes: Int, + @DrawableRes val iconResId: Int, + val destructive: Boolean = false) : VectorSharedAction { object Separator : EventSharedAction(0, 0) - data class OpenUserProfile(val senderId: String) : + data class OpenUserProfile(val userId: String) : EventSharedAction(0, 0) data class AddReaction(val eventId: String) : @@ -51,10 +53,10 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic EventSharedAction(R.string.global_retry, R.drawable.ic_refresh_cw) data class Remove(val eventId: String) : - EventSharedAction(R.string.remove, R.drawable.ic_trash) + EventSharedAction(R.string.remove, R.drawable.ic_trash, true) - data class Delete(val eventId: String) : - EventSharedAction(R.string.delete, R.drawable.ic_delete) + data class Redact(val eventId: String, val askForReason: Boolean) : + EventSharedAction(R.string.message_action_item_redact, R.drawable.ic_delete, true) data class Cancel(val eventId: String) : EventSharedAction(R.string.cancel, R.drawable.ic_close_round) @@ -81,7 +83,7 @@ sealed class EventSharedAction(@StringRes val titleRes: Int, @DrawableRes val ic EventSharedAction(R.string.report_content_custom, R.drawable.ic_report_custom) data class IgnoreUser(val senderId: String?) : - EventSharedAction(R.string.message_ignore_user, R.drawable.ic_alert_triangle) + EventSharedAction(R.string.message_ignore_user, R.drawable.ic_alert_triangle, true) data class QuickReact(val eventId: String, val clickedOn: String, val add: Boolean) : EventSharedAction(0, 0) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt index bdfdb02be1..d2cdf37eab 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsEpoxyController.kt @@ -111,7 +111,7 @@ class MessageActionsEpoxyController @Inject constructor( showExpand(action is EventSharedAction.ReportContent) expanded(state.expendedReportContentMenu) listener(View.OnClickListener { listener?.didSelectMenuAction(action) }) - destructive(action is EventSharedAction.IgnoreUser) + destructive(action.destructive) } if (action is EventSharedAction.ReportContent && state.expendedReportContentMenu) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt index 114d80d9af..a36215007d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/action/MessageActionsViewModel.kt @@ -168,6 +168,10 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } private fun computeMessageBody(timelineEvent: TimelineEvent): CharSequence { + if (timelineEvent.root.isRedacted()) { + return getRedactionReason(timelineEvent) + } + return when (timelineEvent.root.getClearType()) { EventType.MESSAGE, EventType.STICKER -> { @@ -200,6 +204,31 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } ?: "" } + private fun getRedactionReason(timelineEvent: TimelineEvent): String { + return (timelineEvent + .root + .unsignedData + ?.redactedEvent + ?.content + ?.get("reason") as? String) + ?.takeIf { it.isNotBlank() } + .let { reason -> + if (reason == null) { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason) + } + } else { + if (timelineEvent.root.isRedactedBySameUser()) { + stringProvider.getString(R.string.event_redacted_by_user_reason_with_reason, reason) + } else { + stringProvider.getString(R.string.event_redacted_by_admin_reason_with_reason, reason) + } + } + } + } + private fun actionsForEvent(timelineEvent: TimelineEvent): List { val messageContent: MessageContent? = timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: timelineEvent.root.getClearContent().toModel() @@ -227,7 +256,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted } if (canRedact(timelineEvent, session.myUserId)) { - add(EventSharedAction.Delete(eventId)) + add(EventSharedAction.Redact(eventId, askForReason = informationData.senderId != session.myUserId)) } if (canCopy(msgType)) { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt index 65a6f5f244..83f0c63147 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -33,10 +33,14 @@ import im.vector.matrix.android.api.session.room.model.message.MessageEmoteConte import im.vector.matrix.android.api.session.room.model.message.MessageFileContent import im.vector.matrix.android.api.session.room.model.message.MessageImageInfoContent import im.vector.matrix.android.api.session.room.model.message.MessageNoticeContent +import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent +import im.vector.matrix.android.api.session.room.model.message.MessagePollResponseContent import im.vector.matrix.android.api.session.room.model.message.MessageTextContent import im.vector.matrix.android.api.session.room.model.message.MessageType import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent import im.vector.matrix.android.api.session.room.model.message.MessageVideoContent +import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_BUTTONS +import im.vector.matrix.android.api.session.room.model.message.OPTION_TYPE_POLL import im.vector.matrix.android.api.session.room.model.message.getFileUrl import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent @@ -57,7 +61,6 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformat import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotx.features.home.room.detail.timeline.item.AbsMessageItem -import im.vector.riotx.features.home.room.detail.timeline.item.DefaultItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageBlockCodeItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem @@ -65,6 +68,8 @@ import im.vector.riotx.features.home.room.detail.timeline.item.MessageFileItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageImageVideoItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.MessageOptionsItem_ +import im.vector.riotx.features.home.room.detail.timeline.item.MessagePollItem_ import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem import im.vector.riotx.features.home.room.detail.timeline.item.MessageTextItem_ import im.vector.riotx.features.home.room.detail.timeline.item.RedactedMessageItem @@ -121,7 +126,7 @@ class MessageItemFactory @Inject constructor( if (messageContent.relatesTo?.type == RelationType.REPLACE || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { - // This is an edit event, we should it when debugging as a notice event + // This is an edit event, we should display it when debugging as a notice event return noticeItemFactory.create(event, highlight, callback) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) @@ -137,7 +142,40 @@ class MessageItemFactory @Inject constructor( is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) - else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) + is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) + is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) + else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) + } + } + + private fun buildOptionsMessageItem(messageContent: MessageOptionsContent, + informationData: MessageInformationData, + highlight: Boolean, + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): VectorEpoxyModel<*>? { + return when (messageContent.optionType) { + OPTION_TYPE_POLL -> { + MessagePollItem_() + .attributes(attributes) + .callback(callback) + .informationData(informationData) + .leftGuideline(avatarSizeProvider.leftGuideline) + .optionsContent(messageContent) + .highlighted(highlight) + } + OPTION_TYPE_BUTTONS -> { + MessageOptionsItem_() + .attributes(attributes) + .callback(callback) + .informationData(informationData) + .leftGuideline(avatarSizeProvider.leftGuideline) + .optionsContent(messageContent) + .highlighted(highlight) + } + else -> { + // Not supported optionType + buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) + } } } @@ -181,7 +219,6 @@ class MessageItemFactory @Inject constructor( VerificationRequestItem.Attributes( otherUserId = otherUserId, otherUserName = otherUserName.toString(), - fromDevide = messageContent.fromDevice ?: "", referenceId = informationData.eventId, informationData = informationData, avatarRenderer = attributes.avatarRenderer, @@ -228,9 +265,10 @@ class MessageItemFactory @Inject constructor( private fun buildNotHandledMessageItem(messageContent: MessageContent, informationData: MessageInformationData, highlight: Boolean, - callback: TimelineEventController.Callback?): DefaultItem? { - val text = stringProvider.getString(R.string.rendering_event_error_type_of_message_not_handled, messageContent.msgType) - return defaultItemFactory.create(text, informationData, highlight, callback) + callback: TimelineEventController.Callback?, + attributes: AbsMessageItem.Attributes): MessageTextItem? { + // For compatibility reason we should display the body + return buildMessageTextItem(messageContent.body, false, informationData, highlight, callback, attributes) } private fun buildImageMessageItem(messageContent: MessageImageInfoContent, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index cbbfd7c320..c300b8e1c3 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -30,6 +30,7 @@ import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.resources.ColorProvider import im.vector.riotx.core.utils.getColorFromUserId import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData +import im.vector.riotx.features.home.room.detail.timeline.item.PollResponseData import im.vector.riotx.features.home.room.detail.timeline.item.ReactionInfoData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReferencesInfoData @@ -82,6 +83,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ?.map { ReactionInfoData(it.key, it.count, it.addedByMe, it.localEchoEvents.isEmpty()) }, + pollResponseAggregatedSummary = event.annotations?.pollResponseSummary?.let { + PollResponseData( + myVote = it.aggregatedContent?.myVote, + isClosed = it.closedTime ?: Long.MAX_VALUE > System.currentTimeMillis(), + votes = it.aggregatedContent?.votes + ?.groupBy({ it.optionIndex }, { it.userId }) + ?.mapValues { it.value.size } + ) + }, hasBeenEdited = event.hasBeenEdited(), hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, readReceipts = event.readReceipts diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt index fe5d0d03ca..8d4ae81201 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -34,6 +34,8 @@ data class MessageInformationData( val showInformation: Boolean = true, /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, + val pollResponseAggregatedSummary: PollResponseData? = null, + val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, val readReceipts: List = emptyList(), @@ -66,4 +68,11 @@ data class ReadReceiptData( val timestamp: Long ) : Parcelable +@Parcelize +data class PollResponseData( + val myVote: Int?, + val votes: Map?, + val isClosed: Boolean = false +) : Parcelable + fun ReadReceiptData.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt new file mode 100644 index 0000000000..080ff26992 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessageOptionsItem.kt @@ -0,0 +1,78 @@ +/* + * Copyright 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.features.home.room.detail.timeline.item + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.button.MaterialButton +import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.features.home.room.detail.RoomDetailAction +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessageOptionsItem : AbsMessageItem() { + + @EpoxyAttribute + var optionsContent: MessageOptionsContent? = null + + @EpoxyAttribute + var callback: TimelineEventController.Callback? = null + + @EpoxyAttribute + var informationData: MessageInformationData? = null + + override fun getViewType() = STUB_ID + + override fun bind(holder: Holder) { + super.bind(holder) + + renderSendState(holder.view, holder.labelText) + + holder.labelText.setTextOrHide(optionsContent?.label) + + holder.buttonContainer.removeAllViews() + + val relatedEventId = informationData?.eventId ?: return + val options = optionsContent?.options?.takeIf { it.isNotEmpty() } ?: return + // Now add back the buttons + options.forEachIndexed { index, option -> + val materialButton = LayoutInflater.from(holder.view.context).inflate(R.layout.option_buttons, holder.buttonContainer, false) + as MaterialButton + holder.buttonContainer.addView(materialButton, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + materialButton.text = option.label + materialButton.setOnClickListener { + callback?.onTimelineItemAction(RoomDetailAction.ReplyToOptions(relatedEventId, index, option.value ?: "$index")) + } + } + } + + class Holder : AbsMessageItem.Holder(STUB_ID) { + + val labelText by bind(R.id.optionLabelText) + + val buttonContainer by bind(R.id.optionsButtonContainer) + } + + companion object { + private const val STUB_ID = R.id.messageOptionsStub + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt new file mode 100644 index 0000000000..5c92c22935 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MessagePollItem.kt @@ -0,0 +1,158 @@ +/* + * Copyright 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.features.home.room.detail.timeline.item + +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import im.vector.matrix.android.api.session.room.model.message.MessageOptionsContent +import im.vector.riotx.R +import im.vector.riotx.core.extensions.setTextOrHide +import im.vector.riotx.core.utils.DebouncedClickListener +import im.vector.riotx.features.home.room.detail.RoomDetailAction +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController +import kotlin.math.roundToInt + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base) +abstract class MessagePollItem : AbsMessageItem() { + + @EpoxyAttribute + var optionsContent: MessageOptionsContent? = null + + @EpoxyAttribute + var callback: TimelineEventController.Callback? = null + + @EpoxyAttribute + var informationData: MessageInformationData? = null + + override fun getViewType() = STUB_ID + + override fun bind(holder: Holder) { + super.bind(holder) + + holder.pollId = informationData?.eventId + holder.callback = callback + holder.optionValues = optionsContent?.options?.map { it.value ?: it.label } + + renderSendState(holder.view, holder.labelText) + + holder.labelText.setTextOrHide(optionsContent?.label) + + val buttons = listOf(holder.button1, holder.button2, holder.button3, holder.button4, holder.button5) + val resultLines = listOf(holder.result1, holder.result2, holder.result3, holder.result4, holder.result5) + + buttons.forEach { it.isVisible = false } + resultLines.forEach { it.isVisible = false } + + val myVote = informationData?.pollResponseAggregatedSummary?.myVote + val iHaveVoted = myVote != null + val votes = informationData?.pollResponseAggregatedSummary?.votes + val totalVotes = votes?.values + ?.fold(0) { acc, count -> acc + count } ?: 0 + val percentMode = totalVotes > 100 + + if (!iHaveVoted) { + // Show buttons if i have not voted + holder.resultWrapper.isVisible = false + optionsContent?.options?.forEachIndexed { index, item -> + if (index < buttons.size) { + buttons[index].let { + it.text = item.label + it.isVisible = true + } + } + } + } else { + holder.resultWrapper.isVisible = true + val maxCount = votes?.maxBy { it.value }?.value ?: 0 + optionsContent?.options?.forEachIndexed { index, item -> + if (index < resultLines.size) { + val optionCount = votes?.get(index) ?: 0 + val count = if (percentMode) { + if (totalVotes > 0) { + (optionCount / totalVotes.toFloat() * 100).roundToInt().let { "$it%" } + } else { + "" + } + } else { + optionCount.toString() + } + resultLines[index].let { + it.label = item.label + it.isWinner = optionCount == maxCount + it.optionSelected = index == myVote + it.percent = count + it.isVisible = true + } + } + } + } + holder.infoText.text = holder.view.context.resources.getQuantityString(R.plurals.poll_info, totalVotes, totalVotes) + } + + override fun unbind(holder: Holder) { + holder.pollId = null + holder.callback = null + holder.optionValues = null + super.unbind(holder) + } + + class Holder : AbsMessageItem.Holder(STUB_ID) { + + var pollId: String? = null + var optionValues: List? = null + var callback: TimelineEventController.Callback? = null + + val button1 by bind