diff --git a/CHANGES.md b/CHANGES.md index 27229361a7..b4691a907b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,11 +5,15 @@ Features ✨: - Enable url previews for notices (#2562) Improvements 🙌: - - + - Add System theme option and set as default (#904) (#2387) Bugfix 🐛: + - Unspecced msgType field in m.sticker (#2580) + - Wait for all room members to be known before sending a message to a e2e room (#2518) - Url previews sometimes attached to wrong message (#2561) - Room Topic not displayed correctly after visiting a link (#2551) + - Hiding membership events works the exact opposite (#2603) + - Tapping drawer having more than 1 room in notifications gives "malformed link" error (#2605) Translations 🗣: - diff --git a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt index ae095be41a..9b1345cd39 100644 --- a/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt +++ b/attachment-viewer/src/main/java/im/vector/lib/attachmentviewer/AttachmentViewerActivity.kt @@ -40,13 +40,16 @@ import kotlin.math.abs abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener { - lateinit var pager2: ViewPager2 - lateinit var imageTransitionView: ImageView - lateinit var transitionImageContainer: ViewGroup + protected val pager2: ViewPager2 + get() = views.attachmentPager + protected val imageTransitionView: ImageView + get() = views.transitionImageView + protected val transitionImageContainer: ViewGroup + get() = views.transitionImageContainer - var topInset = 0 - var bottomInset = 0 - var systemUiVisibility = true + private var topInset = 0 + private var bottomInset = 0 + private var systemUiVisibility = true private var overlayView: View? = null set(value) { @@ -65,14 +68,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi private lateinit var gestureDetector: GestureDetectorCompat var currentPosition = 0 + private set private var swipeDirection: SwipeDirection? = null private fun isScaled() = attachmentsAdapter.isScaled(currentPosition) + private val attachmentsAdapter = AttachmentsAdapter() + private var wasScaled: Boolean = false private var isSwipeToDismissAllowed: Boolean = true - private lateinit var attachmentsAdapter: AttachmentsAdapter private var isOverlayWasClicked = false // private val shouldDismissToBottom: Boolean @@ -101,10 +106,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi views = ActivityAttachmentViewerBinding.inflate(layoutInflater) setContentView(views.root) views.attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL - attachmentsAdapter = AttachmentsAdapter() views.attachmentPager.adapter = attachmentsAdapter - imageTransitionView = views.transitionImageView - pager2 = views.attachmentPager directionDetector = createSwipeDirectionDetector() gestureDetector = createGestureDetector() diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt index 0e7088a6a5..cb49ee8818 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CommonTestHelper.kt @@ -86,7 +86,7 @@ class CommonTestHelper(context: Context) { * * @param session the session to sync */ - fun syncSession(session: Session) { + fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis) { val lock = CountDownLatch(1) val job = GlobalScope.launch(Dispatchers.Main) { @@ -109,7 +109,7 @@ class CommonTestHelper(context: Context) { } GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) } - await(lock) + await(lock, timeout) } /** @@ -119,7 +119,7 @@ class CommonTestHelper(context: Context) { * @param message the message to send * @param nbOfMessages the number of time the message will be sent */ - fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List { + fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List { val timeline = room.createTimeline(null, TimelineSettings(10)) val sentEvents = ArrayList(nbOfMessages) val latch = CountDownLatch(1) @@ -151,7 +151,7 @@ class CommonTestHelper(context: Context) { room.sendTextMessage(message + " #" + (i + 1)) } // Wait 3 second more per message - await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) + await(latch, timeout = timeout + 3_000L * nbOfMessages) timeline.dispose() // Check that all events has been created @@ -215,14 +215,14 @@ class CommonTestHelper(context: Context) { .getLoginFlow(hs, it) } - doSync { + doSync(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .createAccount(userName, password, null, it) } // Perform dummy step - val registrationResult = doSync { + val registrationResult = doSync(timeout = 60_000) { matrix.authenticationService .getRegistrationWizard() .dummy(it) @@ -231,7 +231,7 @@ class CommonTestHelper(context: Context) { assertTrue(registrationResult is RegistrationResult.Success) val session = (registrationResult as RegistrationResult.Success).session if (sessionTestParams.withInitialSync) { - syncSession(session) + syncSession(session, 60_000) } return session diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt index 76e59d9a90..b6bedbd719 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestData.kt @@ -18,14 +18,21 @@ package org.matrix.android.sdk.common import org.matrix.android.sdk.api.session.Session -data class CryptoTestData(val firstSession: Session, - val roomId: String, - val secondSession: Session? = null, - val thirdSession: Session? = null) { +data class CryptoTestData(val roomId: String, + val sessions: List) { + + val firstSession: Session + get() = sessions.first() + + val secondSession: Session? + get() = sessions.getOrNull(1) + + val thirdSession: Session? + get() = sessions.getOrNull(2) fun cleanUp(testHelper: CommonTestHelper) { - testHelper.signOutAndClose(firstSession) - secondSession?.let { testHelper.signOutAndClose(it) } - thirdSession?.let { testHelper.signOutAndClose(it) } + sessions.forEach { + testHelper.signOutAndClose(it) + } } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt index cbb22daf0f..3d5856fc64 100644 --- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/common/CryptoTestHelper.kt @@ -73,7 +73,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } - return CryptoTestData(aliceSession, roomId) + return CryptoTestData(roomId, listOf(aliceSession)) } /** @@ -139,7 +139,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // assertNotNull(roomFromBobPOV.powerLevels) // assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) - return CryptoTestData(aliceSession, aliceRoomId, bobSession) + return CryptoTestData(aliceRoomId, listOf(aliceSession, bobSession)) } /** @@ -157,7 +157,7 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { // wait the initial sync SystemClock.sleep(1000) - return CryptoTestData(aliceSession, aliceRoomId, cryptoTestData.secondSession, samSession) + return CryptoTestData(aliceRoomId, listOf(aliceSession, cryptoTestData.secondSession!!, samSession)) } /** @@ -381,4 +381,30 @@ class CryptoTestHelper(private val mTestHelper: CommonTestHelper) { } } } + + fun doE2ETestWithManyMembers(numberOfMembers: Int): CryptoTestData { + val aliceSession = mTestHelper.createAccount(TestConstants.USER_ALICE, defaultSessionParams) + aliceSession.cryptoService().setWarnOnUnknownDevices(false) + + val roomId = mTestHelper.doSync { + aliceSession.createRoom(CreateRoomParams().apply { name = "MyRoom" }, it) + } + val room = aliceSession.getRoom(roomId)!! + + mTestHelper.runBlockingTest { + room.enableEncryption() + } + + val sessions = mutableListOf(aliceSession) + for (index in 1 until numberOfMembers) { + val session = mTestHelper.createAccount("User_$index", defaultSessionParams) + mTestHelper.doSync(timeout = 600_000) { room.invite(session.myUserId, null, it) } + println("TEST -> " + session.myUserId + " invited") + mTestHelper.doSync { session.joinRoom(room.roomId, null, emptyList(), it) } + println("TEST -> " + session.myUserId + " joined") + sessions.add(session) + } + + return CryptoTestData(roomId, sessions) + } } diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt new file mode 100644 index 0000000000..ff07cf1d1d --- /dev/null +++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/session/room/timeline/TimelineWithManyMembersTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.session.room.timeline + +import org.junit.FixMethodOrder +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.runners.MethodSorters +import org.matrix.android.sdk.InstrumentedTest +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.message.MessageContent +import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings +import org.matrix.android.sdk.common.CommonTestHelper +import org.matrix.android.sdk.common.CryptoTestHelper +import java.util.concurrent.CountDownLatch +import kotlin.test.fail + +@RunWith(JUnit4::class) +@FixMethodOrder(MethodSorters.JVM) +class TimelineWithManyMembersTest : InstrumentedTest { + + companion object { + private const val NUMBER_OF_MEMBERS = 6 + } + + private val commonTestHelper = CommonTestHelper(context()) + private val cryptoTestHelper = CryptoTestHelper(commonTestHelper) + + /** + * Ensures when someone sends a message to a crowded room, everyone can decrypt the message. + */ + @Test + fun everyone_should_decrypt_message_in_a_crowded_room() { + val cryptoTestData = cryptoTestHelper.doE2ETestWithManyMembers(NUMBER_OF_MEMBERS) + + val sessionForFirstMember = cryptoTestData.firstSession + val roomForFirstMember = sessionForFirstMember.getRoom(cryptoTestData.roomId)!! + + val firstMessage = "First messages from Alice" + commonTestHelper.sendTextMessage( + roomForFirstMember, + firstMessage, + 1, + 600_000 + ) + + for (index in 1 until cryptoTestData.sessions.size) { + val session = cryptoTestData.sessions[index] + val roomForCurrentMember = session.getRoom(cryptoTestData.roomId)!! + val timelineForCurrentMember = roomForCurrentMember.createTimeline(null, TimelineSettings(30)) + timelineForCurrentMember.start() + + session.startSync(true) + + run { + val lock = CountDownLatch(1) + val eventsListener = commonTestHelper.createEventListener(lock) { snapshot -> + snapshot + .find { it.isEncrypted() } + ?.let { + val body = it.root.getClearContent()?.toModel()?.body + if (body?.startsWith(firstMessage).orFalse()) { + println("User " + session.myUserId + " decrypted as " + body) + return@createEventListener true + } else { + fail("User " + session.myUserId + " decrypted as " + body + " CryptoError: " + it.root.mCryptoError) + } + } ?: return@createEventListener false + } + timelineForCurrentMember.addListener(eventsListener) + commonTestHelper.await(lock, 600_000) + } + session.stopSync() + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt index 360b955869..bf21941e0c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/AuthenticationService.kt @@ -41,6 +41,16 @@ interface AuthenticationService { */ fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback): Cancelable + /** + * Get a SSO url + */ + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? + + /** + * Get the sign in or sign up fallback URL + */ + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? + /** * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt index aefc086b43..ac1d726d03 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt @@ -25,7 +25,6 @@ interface PermalinkService { companion object { const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" - const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" } /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt index 00fa68c0ac..280316d4b5 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageStickerContent.kt @@ -27,6 +27,7 @@ data class MessageStickerContent( /** * Set in local, not from server */ + @Transient override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL, /** diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt new file mode 100644 index 0000000000..17f27b2514 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/util/UrlExtensions.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.util + +import java.net.URLEncoder + +/** + * Append param and value to a Url, using "?" or "&". Value parameter will be encoded + * Return this for chaining purpose + */ +fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { + if (contains("?")) { + append("&") + } else { + append("?") + } + + append(param) + append("=") + append(URLEncoder.encode(value, "utf-8")) + + return this +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt similarity index 69% rename from matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt rename to matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt index 7d18aba627..642279cc27 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/auth/Constants.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/Constants.kt @@ -14,25 +14,25 @@ * limitations under the License. */ -package org.matrix.android.sdk.api.auth +package org.matrix.android.sdk.internal.auth /** * Path to use when the client does not supported any or all login flows * Ref: https://matrix.org/docs/spec/client_server/latest#login-fallback */ -const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" +internal const val LOGIN_FALLBACK_PATH = "/_matrix/static/client/login/" /** * Path to use when the client does not supported any or all registration flows * Not documented */ -const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" +internal const val REGISTER_FALLBACK_PATH = "/_matrix/static/client/register/" /** * Path to use when the client want to connect using SSO * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login */ -const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" -const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" +internal const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" +internal const val MSC2858_SSO_REDIRECT_PATH = "/_matrix/client/unstable/org.matrix.msc2858/login/sso/redirect" -const val SSO_REDIRECT_URL_PARAM = "redirectUrl" +internal const val SSO_REDIRECT_URL_PARAM = "redirectUrl" diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt index 55f053de8d..c99e9bd81c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/auth/DefaultAuthenticationService.kt @@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.failure.Failure import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.NoOpCancellable +import org.matrix.android.sdk.api.util.appendParamToUrl import org.matrix.android.sdk.internal.SessionManager import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.RiotConfig @@ -99,6 +100,52 @@ internal class DefaultAuthenticationService @Inject constructor( } } + override fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + val homeServerUrlBase = getHomeServerUrlBase() ?: return null + + return buildString { + append(homeServerUrlBase) + if (providerId != null) { + append(MSC2858_SSO_REDIRECT_PATH) + append("/$providerId") + } else { + append(SSO_REDIRECT_PATH) + } + // Set the redirect url + appendParamToUrl(SSO_REDIRECT_URL_PARAM, redirectUrl) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } + } + + override fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + val homeServerUrlBase = getHomeServerUrlBase() ?: return null + + return buildString { + append(homeServerUrlBase) + if (forSignIn) { + append(LOGIN_FALLBACK_PATH) + deviceId?.takeIf { it.isNotBlank() }?.let { + // But https://github.com/matrix-org/synapse/issues/5755 + appendParamToUrl("device_id", it) + } + } else { + // For sign up + append(REGISTER_FALLBACK_PATH) + } + } + } + + private fun getHomeServerUrlBase(): String? { + return pendingSessionData + ?.homeServerConnectionConfig + ?.homeServerUri + ?.toString() + ?.trim { it == '/' } + } + override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback): Cancelable { pendingSessionData = null diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt index 8b739c4b64..5c8c7dfb25 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/tasks/SendEventTask.kt @@ -20,6 +20,7 @@ import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.task.Task @@ -35,11 +36,19 @@ internal interface SendEventTask : Task { internal class DefaultSendEventTask @Inject constructor( private val localEchoRepository: LocalEchoRepository, private val encryptEventTask: DefaultEncryptEventTask, + private val loadRoomMembersTask: LoadRoomMembersTask, private val roomAPI: RoomAPI, private val eventBus: EventBus) : SendEventTask { override suspend fun execute(params: SendEventTask.Params): String { try { + // Make sure to load all members in the room before sending the event. + params.event.roomId + ?.takeIf { params.encrypt } + ?.let { roomId -> + loadRoomMembersTask.execute(LoadRoomMembersTask.Params(roomId)) + } + val event = handleEncryption(params) val localId = event.eventId!! diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index b970ec60e2..57002b5a60 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -21,6 +21,8 @@ import io.realm.RealmMigration import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields import org.matrix.android.sdk.internal.database.model.PreviewUrlCacheEntityFields +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields import timber.log.Timber import javax.inject.Inject @@ -28,7 +30,7 @@ import javax.inject.Inject class RealmSessionStoreMigration @Inject constructor() : RealmMigration { companion object { - const val SESSION_STORE_SCHEMA_VERSION = 6L + const val SESSION_STORE_SCHEMA_VERSION = 7L } override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { @@ -40,6 +42,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 5) migrateTo6(realm) + if (oldVersion <= 6) migrateTo7(realm) } private fun migrateTo1(realm: DynamicRealm) { @@ -105,4 +108,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java) .addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::class.java) } + + private fun migrateTo7(realm: DynamicRealm) { + Timber.d("Step 6 -> 7") + realm.schema.get("RoomEntity") + ?.addField(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, String::class.java) + ?.transform { obj -> + if (obj.getBoolean("areAllMembersLoaded")) { + obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.LOADED.name) + } else { + obj.setString("membersLoadStatusStr", RoomMembersLoadStatusType.NONE.name) + } + } + ?.removeField("areAllMembersLoaded") + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt index 9af1646a4c..3ff2532604 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomEntity.kt @@ -23,8 +23,7 @@ import io.realm.annotations.PrimaryKey internal open class RoomEntity(@PrimaryKey var roomId: String = "", var chunks: RealmList = RealmList(), - var sendingTimelineEvents: RealmList = RealmList(), - var areAllMembersLoaded: Boolean = false + var sendingTimelineEvents: RealmList = RealmList() ) : RealmObject() { private var membershipStr: String = Membership.NONE.name @@ -36,5 +35,14 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "", membershipStr = value.name } + private var membersLoadStatusStr: String = RoomMembersLoadStatusType.NONE.name + var membersLoadStatus: RoomMembersLoadStatusType + get() { + return RoomMembersLoadStatusType.valueOf(membersLoadStatusStr) + } + set(value) { + membersLoadStatusStr = value.name + } + companion object } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt new file mode 100644 index 0000000000..79fe17253b --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomMembersLoadStatusType.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2020 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.internal.database.model + +internal enum class RoomMembersLoadStatusType { + NONE, + LOADING, + LOADED +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt index 627f927ad8..53fa73c624 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/membership/LoadRoomMembersTask.kt @@ -17,12 +17,19 @@ package org.matrix.android.sdk.internal.session.room.membership import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.kotlin.createObject +import kotlinx.coroutines.TimeoutCancellationException +import org.greenrobot.eventbus.EventBus import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.send.SendState +import org.matrix.android.sdk.internal.database.awaitNotEmptyResult import org.matrix.android.sdk.internal.database.mapper.toEntity import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity import org.matrix.android.sdk.internal.database.model.EventInsertType import org.matrix.android.sdk.internal.database.model.RoomEntity +import org.matrix.android.sdk.internal.database.model.RoomEntityFields +import org.matrix.android.sdk.internal.database.model.RoomMembersLoadStatusType import org.matrix.android.sdk.internal.database.query.copyToRealmOrIgnore import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.where @@ -33,9 +40,7 @@ import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater import org.matrix.android.sdk.internal.session.sync.SyncTokenStore import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import io.realm.kotlin.createObject -import org.greenrobot.eventbus.EventBus +import java.util.concurrent.TimeUnit import javax.inject.Inject internal interface LoadRoomMembersTask : Task { @@ -56,13 +61,40 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( ) : LoadRoomMembersTask { override suspend fun execute(params: LoadRoomMembersTask.Params) { - if (areAllMembersAlreadyLoaded(params.roomId)) { - return + when (getRoomMembersLoadStatus(params.roomId)) { + RoomMembersLoadStatusType.NONE -> doRequest(params) + RoomMembersLoadStatusType.LOADING -> waitPreviousRequestToFinish(params) + RoomMembersLoadStatusType.LOADED -> Unit } + } + + private suspend fun waitPreviousRequestToFinish(params: LoadRoomMembersTask.Params) { + try { + awaitNotEmptyResult(monarchy.realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm -> + realm.where(RoomEntity::class.java) + .equalTo(RoomEntityFields.ROOM_ID, params.roomId) + .equalTo(RoomEntityFields.MEMBERS_LOAD_STATUS_STR, RoomMembersLoadStatusType.LOADED.name) + } + } catch (exception: TimeoutCancellationException) { + // Timeout, do the request anyway (?) + doRequest(params) + } + } + + private suspend fun doRequest(params: LoadRoomMembersTask.Params) { + setRoomMembersLoadStatus(params.roomId, RoomMembersLoadStatusType.LOADING) + val lastToken = syncTokenStore.getLastToken() - val response = executeRequest(eventBus) { - apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + val response = try { + executeRequest(eventBus) { + apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) + } + } catch (throwable: Throwable) { + // Revert status to NONE + setRoomMembersLoadStatus(params.roomId, RoomMembersLoadStatusType.NONE) + throw throwable } + // This will also set the status to LOADED insertInDb(response, params.roomId) } @@ -84,14 +116,23 @@ internal class DefaultLoadRoomMembersTask @Inject constructor( } roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) } - roomEntity.areAllMembersLoaded = true + roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED roomSummaryUpdater.update(realm, roomId, updateMembers = true) } } - private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { - return Realm.getInstance(monarchy.realmConfiguration).use { - RoomEntity.where(it, roomId).findFirst()?.areAllMembersLoaded ?: false + private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType { + var result: RoomMembersLoadStatusType? + Realm.getInstance(monarchy.realmConfiguration).use { + result = RoomEntity.where(it, roomId).findFirst()?.membersLoadStatus + } + return result ?: RoomMembersLoadStatusType.NONE + } + + private suspend fun setRoomMembersLoadStatus(roomId: String, status: RoomMembersLoadStatusType) { + monarchy.awaitTransaction { realm -> + val roomEntity = RoomEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) + roomEntity.membersLoadStatus = status } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 86b0497bd0..dd58529412 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -27,6 +27,7 @@ import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import org.matrix.android.sdk.api.MatrixCallback +import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType @@ -53,6 +54,7 @@ import org.matrix.android.sdk.internal.database.query.filterEvents import org.matrix.android.sdk.internal.database.query.findAllInRoomWithSendStates import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.whereRoomId +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.util.Debouncer @@ -81,7 +83,8 @@ internal class DefaultTimeline( private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val eventBus: EventBus, private val eventDecryptor: TimelineEventDecryptor, - private val realmSessionProvider: RealmSessionProvider + private val realmSessionProvider: RealmSessionProvider, + private val loadRoomMembersTask: LoadRoomMembersTask ) : Timeline, TimelineHiddenReadReceipts.Delegate { data class OnNewTimelineEvents(val roomId: String, val eventIds: List) @@ -184,6 +187,13 @@ internal class DefaultTimeline( if (settings.shouldHandleHiddenReadReceipts()) { hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) } + + loadRoomMembersTask + .configureWith(LoadRoomMembersTask.Params(roomId)) { + this.callback = NoOpMatrixCallback() + } + .executeBy(taskExecutor) + isReady.set(true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index 783aa53ddf..d02e906d00 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -39,6 +39,7 @@ import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.session.room.membership.LoadRoomMembersTask import org.matrix.android.sdk.internal.task.TaskExecutor internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, @@ -51,7 +52,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv private val paginationTask: PaginationTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper + private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, + private val loadRoomMembersTask: LoadRoomMembersTask ) : TimelineService { @AssistedInject.Factory @@ -73,7 +75,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv eventBus = eventBus, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, - realmSessionProvider = realmSessionProvider + realmSessionProvider = realmSessionProvider, + loadRoomMembersTask = loadRoomMembersTask ) } diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index b9929dfebe..2306eaed8b 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===84 +enum class===85 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt index 38977d33ba..5037f78445 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/UrlExtensions.kt @@ -16,26 +16,6 @@ package im.vector.app.core.extensions -import java.net.URLEncoder - -/** - * Append param and value to a Url, using "?" or "&". Value parameter will be encoded - * Return this for chaining purpose - */ -fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder { - if (contains("?")) { - append("&") - } else { - append("?") - } - - append(param) - append("=") - append(URLEncoder.encode(value, "utf-8")) - - return this -} - /** * Ex: "https://matrix.org/" -> "matrix.org" */ diff --git a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt index 6f261ad717..f86825750a 100644 --- a/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/core/ui/views/BottomSheetActionButton.kt @@ -28,7 +28,7 @@ import androidx.core.view.isInvisible import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.databinding.ItemVerificationActionBinding +import im.vector.app.databinding.ViewBottomSheetActionButtonBinding import im.vector.app.features.themes.ThemeUtils class BottomSheetActionButton @JvmOverloads constructor( @@ -36,7 +36,7 @@ class BottomSheetActionButton @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { - val views : ItemVerificationActionBinding + val views: ViewBottomSheetActionButtonBinding var title: String? = null set(value) { @@ -97,8 +97,8 @@ class BottomSheetActionButton @JvmOverloads constructor( } init { - inflate(context, R.layout.item_verification_action, this) - views = ItemVerificationActionBinding.bind(this) + inflate(context, R.layout.view_bottom_sheet_action_button, this) + views = ViewBottomSheetActionButtonBinding.bind(this) context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) { title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt index 5fdc70c539..9aa6ccd298 100644 --- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt +++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt @@ -18,7 +18,7 @@ package im.vector.app.features.call import android.content.Context import android.util.AttributeSet -import android.widget.LinearLayout +import android.widget.FrameLayout import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.databinding.ViewCallControlsBinding @@ -28,7 +28,7 @@ import org.webrtc.PeerConnection class CallControlsView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { +) : FrameLayout(context, attrs, defStyleAttr) { private val views: ViewCallControlsBinding diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 9d7beb13a3..108e0512a7 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -39,7 +39,6 @@ import im.vector.app.core.extensions.replaceFragment import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager -import im.vector.app.core.utils.toast import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.matrixto.MatrixToBottomSheet @@ -166,8 +165,8 @@ class HomeActivity : private fun handleIntent(intent: Intent?) { intent?.dataString?.let { deepLink -> val resolvedLink = when { - deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink - deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { + deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink + deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { // This is a bit ugly, but for now just convert to matrix.to link for compatibility when { deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) @@ -177,7 +176,7 @@ class HomeActivity : activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) } } - else -> null + else -> return@let } permalinkHandler.launch( @@ -190,7 +189,11 @@ class HomeActivity : .observeOn(AndroidSchedulers.mainThread()) .subscribe { isHandled -> if (!isHandled) { - toast(R.string.permalink_malformed) + AlertDialog.Builder(this) + .setTitle(R.string.dialog_title_error) + .setMessage(R.string.permalink_malformed) + .setPositiveButton(R.string.ok, null) + .show() } } .disposeOnDestroy() @@ -410,7 +413,8 @@ class HomeActivity : } } - private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" - private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" + private const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://" + private const val ROOM_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" + private const val USER_LINK_PREFIX = "${MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index e4e7177e4f..1e6e7c9d14 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -31,7 +31,6 @@ import im.vector.app.R import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider -import im.vector.app.core.utils.subscribeLogError import im.vector.app.features.call.WebRtcPeerConnectionManager import im.vector.app.features.command.CommandParser import im.vector.app.features.command.ParsedCommand @@ -168,7 +167,6 @@ class RoomDetailViewModel @AssistedInject constructor( observePowerLevel() room.getRoomSummaryLive() room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) - room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() // Inform the SDK that the room is displayed session.onRoomDisplayed(initialState.roomId) chatEffectManager.delegate = this diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt index 1983b05ed3..01c7ad3986 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt @@ -57,7 +57,7 @@ class TimelineSettingsFactory @Inject constructor( return map { EventTypeFilter( eventType = it, - stateKey = if (it == EventType.STATE_ROOM_MEMBER && userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null + stateKey = if (it == EventType.STATE_ROOM_MEMBER && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null ) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollResultLineView.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollResultLineView.kt index d5996a65ba..aa864851cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollResultLineView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/PollResultLineView.kt @@ -23,7 +23,7 @@ import android.widget.LinearLayout import androidx.core.content.withStyledAttributes import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.databinding.ItemTimelineEventPollResultItemBinding +import im.vector.app.databinding.ViewPollResultLineBinding class PollResultLineView @JvmOverloads constructor( context: Context, @@ -31,7 +31,7 @@ class PollResultLineView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : LinearLayout(context, attrs, defStyleAttr) { - private val views: ItemTimelineEventPollResultItemBinding + private val views: ViewPollResultLineBinding var label: String? = null set(value) { @@ -60,8 +60,8 @@ class PollResultLineView @JvmOverloads constructor( } init { - inflate(context, R.layout.item_timeline_event_poll_result_item, this) - views = ItemTimelineEventPollResultItemBinding.bind(this) + inflate(context, R.layout.view_poll_result_line, this) + views = ViewPollResultLineBinding.bind(this) orientation = HORIZONTAL context.withStyledAttributes(attrs, R.styleable.PollResultLineView) { diff --git a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt index c20f4ddd23..3fc5037ae7 100644 --- a/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/AbstractSSOLoginFragment.kt @@ -87,7 +87,12 @@ abstract class AbstractSSOLoginFragment : AbstractLoginFragment withState(loginViewModel) { state -> if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { // in this case we can prefetch (not other cases for privacy concerns) - prefetchUrl(state.getSsoUrl(null)) + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { prefetchUrl(it) } } } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt index 503b6d74a6..803fd38983 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginActivity.kt @@ -360,6 +360,9 @@ open class LoginActivity : VectorBaseActivity(), ToolbarCo private const val EXTRA_CONFIG = "EXTRA_CONFIG" + // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string + const val VECTOR_REDIRECT_URL = "element://connect" + fun newIntent(context: Context, loginConfig: LoginConfig?): Intent { return Intent(context, LoginActivity::class.java).apply { putExtra(EXTRA_CONFIG, loginConfig) diff --git a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt index c396e61b1a..3b22e0f206 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginFragment.kt @@ -193,7 +193,12 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment if (state.loginMode is LoginMode.Sso) { - openInCustomTab(state.getSsoUrl(null)) + loginViewModel.getSsoUrl( + redirectUrl = LoginActivity.VECTOR_REDIRECT_URL, + deviceId = state.deviceId, + providerId = null + ) + ?.let { openInCustomTab(it) } } else { loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt index 0a6dbcaae2..666bd21add 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewModel.kt @@ -818,4 +818,12 @@ class LoginViewModel @AssistedInject constructor( fun getInitialHomeServerUrl(): String? { return loginConfig?.homeServerUrl } + + fun getSsoUrl(redirectUrl: String, deviceId: String?, providerId: String?): String? { + return authenticationService.getSsoUrl(redirectUrl, deviceId, providerId) + } + + fun getFallbackUrl(forSignIn: Boolean, deviceId: String?): String? { + return authenticationService.getFallbackUrl(forSignIn, deviceId) + } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt index 5254abf1d9..37ac89794f 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginViewState.kt @@ -22,10 +22,6 @@ import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.Success import com.airbnb.mvrx.Uninitialized -import im.vector.app.core.extensions.appendParamToUrl -import org.matrix.android.sdk.api.auth.MSC2858_SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_PATH -import org.matrix.android.sdk.api.auth.SSO_REDIRECT_URL_PARAM data class LoginViewState( val asyncLoginAction: Async = Uninitialized, @@ -69,27 +65,4 @@ data class LoginViewState( fun isUserLogged(): Boolean { return asyncLoginAction is Success } - - fun getSsoUrl(providerId: String?): String { - return buildString { - append(homeServerUrl?.trim { it == '/' }) - if (providerId != null) { - append(MSC2858_SSO_REDIRECT_PATH) - append("/$providerId") - } else { - append(SSO_REDIRECT_PATH) - } - // Set a redirect url we will intercept later - appendParamToUrl(SSO_REDIRECT_URL_PARAM, VECTOR_REDIRECT_URL) - deviceId?.takeIf { it.isNotBlank() }?.let { - // But https://github.com/matrix-org/synapse/issues/5755 - appendParamToUrl("device_id", it) - } - } - } - - companion object { - // Note that the domain can be displayed to the user for confirmation that he trusts it. So use a human readable string - private const val VECTOR_REDIRECT_URL = "element://connect" - } } diff --git a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt index acf4f706c5..4b03c93321 100644 --- a/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt +++ b/vector/src/main/java/im/vector/app/features/login/LoginWebFragment.kt @@ -33,14 +33,11 @@ import android.webkit.WebViewClient import androidx.appcompat.app.AlertDialog import com.airbnb.mvrx.activityViewModel import im.vector.app.R -import im.vector.app.core.extensions.appendParamToUrl import im.vector.app.core.utils.AssetReader import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.features.signout.soft.SoftLogoutAction import im.vector.app.features.signout.soft.SoftLogoutViewModel -import org.matrix.android.sdk.api.auth.LOGIN_FALLBACK_PATH -import org.matrix.android.sdk.api.auth.REGISTER_FALLBACK_PATH import org.matrix.android.sdk.api.auth.data.Credentials import org.matrix.android.sdk.internal.di.MoshiProvider import timber.log.Timber @@ -119,19 +116,7 @@ class LoginWebFragment @Inject constructor( } private fun launchWebView(state: LoginViewState) { - val url = buildString { - append(state.homeServerUrl?.trim { it == '/' }) - if (state.signMode == SignMode.SignIn) { - append(LOGIN_FALLBACK_PATH) - state.deviceId?.takeIf { it.isNotBlank() }?.let { - // But https://github.com/matrix-org/synapse/issues/5755 - appendParamToUrl("device_id", it) - } - } else { - // MODE_REGISTER - append(REGISTER_FALLBACK_PATH) - } - } + val url = loginViewModel.getFallbackUrl(state.signMode == SignMode.SignIn, state.deviceId) ?: return views.loginWebWebView.loadUrl(url) diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index bba6b9c253..0856ac4b35 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -18,6 +18,8 @@ package im.vector.app.features.themes import android.app.Activity import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources import android.graphics.drawable.Drawable import android.util.TypedValue import android.view.Menu @@ -39,10 +41,14 @@ object ThemeUtils { const val APPLICATION_THEME_KEY = "APPLICATION_THEME_KEY" // the theme possible values + private const val SYSTEM_THEME_VALUE = "system" private const val THEME_DARK_VALUE = "dark" private const val THEME_LIGHT_VALUE = "light" private const val THEME_BLACK_VALUE = "black" + // The default theme + private const val DEFAULT_THEME = SYSTEM_THEME_VALUE + private var currentTheme = AtomicReference(null) private val mColorByAttr = HashMap() @@ -54,13 +60,12 @@ object ThemeUtils { } /** - * @return true if current theme is Light or Status + * @return true if current theme is Light or current theme is System and system theme is light */ fun isLightTheme(context: Context): Boolean { - return when (getApplicationTheme(context)) { - THEME_LIGHT_VALUE -> true - else -> false - } + val theme = getApplicationTheme(context) + return theme == THEME_LIGHT_VALUE + || (theme == SYSTEM_THEME_VALUE && !isSystemDarkTheme(context.resources)) } /** @@ -73,11 +78,11 @@ object ThemeUtils { val currentTheme = this.currentTheme.get() return if (currentTheme == null) { val prefs = DefaultSharedPreferences.getInstance(context) - var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) ?: THEME_LIGHT_VALUE + var themeFromPref = prefs.getString(APPLICATION_THEME_KEY, DEFAULT_THEME) ?: DEFAULT_THEME if (themeFromPref == "status") { - // Migrate to light theme, which is the closest theme - themeFromPref = THEME_LIGHT_VALUE - prefs.edit { putString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) } + // Migrate to the default theme + themeFromPref = DEFAULT_THEME + prefs.edit { putString(APPLICATION_THEME_KEY, DEFAULT_THEME) } } this.currentTheme.set(themeFromPref) themeFromPref @@ -86,6 +91,13 @@ object ThemeUtils { } } + /** + * @return true if system theme is dark + */ + private fun isSystemDarkTheme(resources: Resources): Boolean { + return resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES + } + /** * Update the application theme * @@ -93,11 +105,14 @@ object ThemeUtils { */ fun setApplicationTheme(context: Context, aTheme: String) { currentTheme.set(aTheme) - when (aTheme) { - THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) - THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) - else -> context.setTheme(R.style.AppTheme_Light) - } + context.setTheme( + when (aTheme) { + SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.AppTheme_Dark else R.style.AppTheme_Light + THEME_DARK_VALUE -> R.style.AppTheme_Dark + THEME_BLACK_VALUE -> R.style.AppTheme_Black + else -> R.style.AppTheme_Light + } + ) // Clear the cache mColorByAttr.clear() @@ -110,6 +125,7 @@ object ThemeUtils { */ fun setActivityTheme(activity: Activity, otherThemes: ActivityOtherThemes) { when (getApplicationTheme(activity)) { + SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(activity.resources)) activity.setTheme(otherThemes.dark) THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark) THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black) } @@ -117,40 +133,6 @@ object ThemeUtils { mColorByAttr.clear() } - /** - * Set the TabLayout colors. - * It seems that there is no proper way to manage it with the manifest file. - * - * @param activity the activity - * @param layout the layout - */ - /* - fun setTabLayoutTheme(activity: Activity, layout: TabLayout) { - if (activity is VectorGroupDetailsActivity) { - val textColor: Int - val underlineColor: Int - val backgroundColor: Int - - if (TextUtils.equals(getApplicationTheme(activity), THEME_LIGHT_VALUE)) { - textColor = ContextCompat.getColor(activity, android.R.color.white) - underlineColor = textColor - backgroundColor = ContextCompat.getColor(activity, R.color.tab_groups) - } else if (TextUtils.equals(getApplicationTheme(activity), THEME_STATUS_VALUE)) { - textColor = ContextCompat.getColor(activity, android.R.color.white) - underlineColor = textColor - backgroundColor = getColor(activity, R.attr.colorPrimary) - } else { - textColor = ContextCompat.getColor(activity, R.color.tab_groups) - underlineColor = textColor - backgroundColor = getColor(activity, R.attr.colorPrimary) - } - - layout.setTabTextColors(textColor, textColor) - layout.setSelectedTabIndicatorColor(underlineColor) - layout.setBackgroundColor(backgroundColor) - } - } */ - /** * Translates color attributes to colors * diff --git a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetActionButton.kt b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetActionButton.kt index 00df261095..61bef29d54 100644 --- a/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetActionButton.kt +++ b/vector/src/main/java/im/vector/app/features/workers/signout/SignOutBottomSheetActionButton.kt @@ -20,19 +20,19 @@ import android.content.Context import android.content.res.ColorStateList import android.graphics.drawable.Drawable import android.util.AttributeSet -import android.widget.LinearLayout +import android.widget.FrameLayout import androidx.core.content.withStyledAttributes import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.databinding.ItemSignoutActionBinding +import im.vector.app.databinding.ViewSignOutBottomSheetActionButtonBinding import im.vector.app.features.themes.ThemeUtils class SignOutBottomSheetActionButton @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 -) : LinearLayout(context, attrs, defStyleAttr) { +) : FrameLayout(context, attrs, defStyleAttr) { - private val views: ItemSignoutActionBinding + private val views: ViewSignOutBottomSheetActionButtonBinding var action: (() -> Unit)? = null @@ -67,8 +67,8 @@ class SignOutBottomSheetActionButton @JvmOverloads constructor( } init { - inflate(context, R.layout.item_signout_action, this) - views = ItemSignoutActionBinding.bind(this) + inflate(context, R.layout.view_sign_out_bottom_sheet_action_button, this) + views = ViewSignOutBottomSheetActionButtonBinding.bind(this) context.withStyledAttributes(attrs, R.styleable.SignOutBottomSheetActionButton) { title = getString(R.styleable.SignOutBottomSheetActionButton_actionTitle) ?: "" diff --git a/vector/src/main/res/layout/item_signout_action.xml b/vector/src/main/res/layout/item_signout_action.xml deleted file mode 100644 index b1eb8c1f62..0000000000 --- a/vector/src/main/res/layout/item_signout_action.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_verification_action.xml b/vector/src/main/res/layout/item_verification_action.xml index ae49893792..68ee392cff 100644 --- a/vector/src/main/res/layout/item_verification_action.xml +++ b/vector/src/main/res/layout/item_verification_action.xml @@ -24,10 +24,10 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:src="@drawable/ic_share" - tools:visibility="visible" app:tint="?riotx_text_primary" - tools:ignore="MissingPrefix" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_share" + tools:visibility="visible" /> + tools:ignore="MissingPrefix" + tools:src="@drawable/ic_arrow_right" /> diff --git a/vector/src/main/res/layout/view_bottom_sheet_action_button.xml b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml new file mode 100644 index 0000000000..c0f55df9e6 --- /dev/null +++ b/vector/src/main/res/layout/view_bottom_sheet_action_button.xml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/view_call_controls.xml b/vector/src/main/res/layout/view_call_controls.xml index 435520b9ef..2487f131e3 100644 --- a/vector/src/main/res/layout/view_call_controls.xml +++ b/vector/src/main/res/layout/view_call_controls.xml @@ -1,9 +1,10 @@ - + android:layout_height="wrap_content" + tools:parentTag="android.widget.FrameLayout"> + app:tint="@color/white" + tools:ignore="MissingConstraints,MissingPrefix" /> + app:tint="@color/white" + tools:ignore="MissingConstraints,MissingPrefix" /> + app:tint="?attr/riotx_text_primary" + tools:ignore="MissingConstraints,MissingPrefix" /> + tools:src="@drawable/ic_microphone_on" /> + app:tint="@color/white" + tools:ignore="MissingConstraints,MissingPrefix" /> + tools:ignore="MissingConstraints,MissingPrefix" /> + app:tint="?attr/riotx_text_primary" + tools:ignore="MissingConstraints,MissingPrefix" /> - \ No newline at end of file + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_poll_result_item.xml b/vector/src/main/res/layout/view_poll_result_line.xml similarity index 100% rename from vector/src/main/res/layout/item_timeline_event_poll_result_item.xml rename to vector/src/main/res/layout/view_poll_result_line.xml diff --git a/vector/src/main/res/layout/view_sign_out_bottom_sheet_action_button.xml b/vector/src/main/res/layout/view_sign_out_bottom_sheet_action_button.xml new file mode 100644 index 0000000000..6809cfd119 --- /dev/null +++ b/vector/src/main/res/layout/view_sign_out_bottom_sheet_action_button.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/vector/src/main/res/values/array.xml b/vector/src/main/res/values/array.xml index e56844e2e8..2c4bc135e5 100644 --- a/vector/src/main/res/values/array.xml +++ b/vector/src/main/res/values/array.xml @@ -92,12 +92,14 @@ + @string/system_theme @string/light_theme @string/dark_theme @string/black_them + system light dark black diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 76541460d2..355ac4d6d6 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -8,6 +8,7 @@ Latn + System Default Light Theme Dark Theme Black Theme diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 74b1f882ee..6297b89e6c 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -13,7 +13,7 @@ app:fragment="im.vector.app.features.settings.locale.LocalePickerFragment" />