Merge branch 'develop' into feature/ons/fix_room_topic_scroll

This commit is contained in:
Onuray Sahin 2021-01-05 15:08:15 +03:00 committed by GitHub
commit 474ade01cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 637 additions and 254 deletions

View File

@ -5,11 +5,15 @@ Features ✨:
- Enable url previews for notices (#2562) - Enable url previews for notices (#2562)
Improvements 🙌: Improvements 🙌:
- - Add System theme option and set as default (#904) (#2387)
Bugfix 🐛: 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) - Url previews sometimes attached to wrong message (#2561)
- Room Topic not displayed correctly after visiting a link (#2551) - 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 🗣: Translations 🗣:
- -

View File

@ -40,13 +40,16 @@ import kotlin.math.abs
abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener { abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventListener {
lateinit var pager2: ViewPager2 protected val pager2: ViewPager2
lateinit var imageTransitionView: ImageView get() = views.attachmentPager
lateinit var transitionImageContainer: ViewGroup protected val imageTransitionView: ImageView
get() = views.transitionImageView
protected val transitionImageContainer: ViewGroup
get() = views.transitionImageContainer
var topInset = 0 private var topInset = 0
var bottomInset = 0 private var bottomInset = 0
var systemUiVisibility = true private var systemUiVisibility = true
private var overlayView: View? = null private var overlayView: View? = null
set(value) { set(value) {
@ -65,14 +68,16 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
private lateinit var gestureDetector: GestureDetectorCompat private lateinit var gestureDetector: GestureDetectorCompat
var currentPosition = 0 var currentPosition = 0
private set
private var swipeDirection: SwipeDirection? = null private var swipeDirection: SwipeDirection? = null
private fun isScaled() = attachmentsAdapter.isScaled(currentPosition) private fun isScaled() = attachmentsAdapter.isScaled(currentPosition)
private val attachmentsAdapter = AttachmentsAdapter()
private var wasScaled: Boolean = false private var wasScaled: Boolean = false
private var isSwipeToDismissAllowed: Boolean = true private var isSwipeToDismissAllowed: Boolean = true
private lateinit var attachmentsAdapter: AttachmentsAdapter
private var isOverlayWasClicked = false private var isOverlayWasClicked = false
// private val shouldDismissToBottom: Boolean // private val shouldDismissToBottom: Boolean
@ -101,10 +106,7 @@ abstract class AttachmentViewerActivity : AppCompatActivity(), AttachmentEventLi
views = ActivityAttachmentViewerBinding.inflate(layoutInflater) views = ActivityAttachmentViewerBinding.inflate(layoutInflater)
setContentView(views.root) setContentView(views.root)
views.attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL views.attachmentPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
attachmentsAdapter = AttachmentsAdapter()
views.attachmentPager.adapter = attachmentsAdapter views.attachmentPager.adapter = attachmentsAdapter
imageTransitionView = views.transitionImageView
pager2 = views.attachmentPager
directionDetector = createSwipeDirectionDetector() directionDetector = createSwipeDirectionDetector()
gestureDetector = createGestureDetector() gestureDetector = createGestureDetector()

View File

@ -86,7 +86,7 @@ class CommonTestHelper(context: Context) {
* *
* @param session the session to sync * @param session the session to sync
*/ */
fun syncSession(session: Session) { fun syncSession(session: Session, timeout: Long = TestConstants.timeOutMillis) {
val lock = CountDownLatch(1) val lock = CountDownLatch(1)
val job = GlobalScope.launch(Dispatchers.Main) { val job = GlobalScope.launch(Dispatchers.Main) {
@ -109,7 +109,7 @@ class CommonTestHelper(context: Context) {
} }
GlobalScope.launch(Dispatchers.Main) { syncLiveData.observeForever(syncObserver) } 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 message the message to send
* @param nbOfMessages the number of time the message will be sent * @param nbOfMessages the number of time the message will be sent
*/ */
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int): List<TimelineEvent> { fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
val timeline = room.createTimeline(null, TimelineSettings(10)) val timeline = room.createTimeline(null, TimelineSettings(10))
val sentEvents = ArrayList<TimelineEvent>(nbOfMessages) val sentEvents = ArrayList<TimelineEvent>(nbOfMessages)
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -151,7 +151,7 @@ class CommonTestHelper(context: Context) {
room.sendTextMessage(message + " #" + (i + 1)) room.sendTextMessage(message + " #" + (i + 1))
} }
// Wait 3 second more per message // Wait 3 second more per message
await(latch, timeout = TestConstants.timeOutMillis + 3_000L * nbOfMessages) await(latch, timeout = timeout + 3_000L * nbOfMessages)
timeline.dispose() timeline.dispose()
// Check that all events has been created // Check that all events has been created
@ -215,14 +215,14 @@ class CommonTestHelper(context: Context) {
.getLoginFlow(hs, it) .getLoginFlow(hs, it)
} }
doSync<RegistrationResult> { doSync<RegistrationResult>(timeout = 60_000) {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()
.createAccount(userName, password, null, it) .createAccount(userName, password, null, it)
} }
// Perform dummy step // Perform dummy step
val registrationResult = doSync<RegistrationResult> { val registrationResult = doSync<RegistrationResult>(timeout = 60_000) {
matrix.authenticationService matrix.authenticationService
.getRegistrationWizard() .getRegistrationWizard()
.dummy(it) .dummy(it)
@ -231,7 +231,7 @@ class CommonTestHelper(context: Context) {
assertTrue(registrationResult is RegistrationResult.Success) assertTrue(registrationResult is RegistrationResult.Success)
val session = (registrationResult as RegistrationResult.Success).session val session = (registrationResult as RegistrationResult.Success).session
if (sessionTestParams.withInitialSync) { if (sessionTestParams.withInitialSync) {
syncSession(session) syncSession(session, 60_000)
} }
return session return session

View File

@ -18,14 +18,21 @@ package org.matrix.android.sdk.common
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
data class CryptoTestData(val firstSession: Session, data class CryptoTestData(val roomId: String,
val roomId: String, val sessions: List<Session>) {
val secondSession: Session? = null,
val thirdSession: Session? = null) { val firstSession: Session
get() = sessions.first()
val secondSession: Session?
get() = sessions.getOrNull(1)
val thirdSession: Session?
get() = sessions.getOrNull(2)
fun cleanUp(testHelper: CommonTestHelper) { fun cleanUp(testHelper: CommonTestHelper) {
testHelper.signOutAndClose(firstSession) sessions.forEach {
secondSession?.let { testHelper.signOutAndClose(it) } testHelper.signOutAndClose(it)
thirdSession?.let { testHelper.signOutAndClose(it) } }
} }
} }

View File

@ -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) // assertNotNull(roomFromBobPOV.powerLevels)
// assertTrue(roomFromBobPOV.powerLevels.maySendMessage(bobSession.myUserId)) // 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 // wait the initial sync
SystemClock.sleep(1000) 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<String> {
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<Unit>(timeout = 600_000) { room.invite(session.myUserId, null, it) }
println("TEST -> " + session.myUserId + " invited")
mTestHelper.doSync<Unit> { session.joinRoom(room.roomId, null, emptyList(), it) }
println("TEST -> " + session.myUserId + " joined")
sessions.add(session)
}
return CryptoTestData(roomId, sessions)
}
} }

View File

@ -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<MessageContent>()?.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()
}
}
}

View File

@ -41,6 +41,16 @@ interface AuthenticationService {
*/ */
fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): Cancelable fun getLoginFlowOfSession(sessionId: String, callback: MatrixCallback<LoginFlowResult>): 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. * Return a LoginWizard, to login to the homeserver. The login flow has to be retrieved first.
*/ */

View File

@ -25,7 +25,6 @@ interface PermalinkService {
companion object { companion object {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
} }
/** /**

View File

@ -27,6 +27,7 @@ data class MessageStickerContent(
/** /**
* Set in local, not from server * Set in local, not from server
*/ */
@Transient
override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL, override val msgType: String = MessageType.MSGTYPE_STICKER_LOCAL,
/** /**

View File

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

View File

@ -14,25 +14,25 @@
* limitations under the License. * 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 * 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 * 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 * Path to use when the client does not supported any or all registration flows
* Not documented * 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 * Path to use when the client want to connect using SSO
* Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login * Ref: https://matrix.org/docs/spec/client_server/latest#sso-client-login
*/ */
const val SSO_REDIRECT_PATH = "/_matrix/client/r0/login/sso/redirect" internal 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 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"

View File

@ -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.session.Session
import org.matrix.android.sdk.api.util.Cancelable import org.matrix.android.sdk.api.util.Cancelable
import org.matrix.android.sdk.api.util.NoOpCancellable 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.SessionManager
import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse import org.matrix.android.sdk.internal.auth.data.LoginFlowResponse
import org.matrix.android.sdk.internal.auth.data.RiotConfig 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<LoginFlowResult>): Cancelable { override fun getLoginFlow(homeServerConnectionConfig: HomeServerConnectionConfig, callback: MatrixCallback<LoginFlowResult>): Cancelable {
pendingSessionData = null pendingSessionData = null

View File

@ -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.api.session.room.send.SendState
import org.matrix.android.sdk.internal.network.executeRequest 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.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.LocalEchoRepository
import org.matrix.android.sdk.internal.session.room.send.SendResponse import org.matrix.android.sdk.internal.session.room.send.SendResponse
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
@ -35,11 +36,19 @@ internal interface SendEventTask : Task<SendEventTask.Params, String> {
internal class DefaultSendEventTask @Inject constructor( internal class DefaultSendEventTask @Inject constructor(
private val localEchoRepository: LocalEchoRepository, private val localEchoRepository: LocalEchoRepository,
private val encryptEventTask: DefaultEncryptEventTask, private val encryptEventTask: DefaultEncryptEventTask,
private val loadRoomMembersTask: LoadRoomMembersTask,
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
private val eventBus: EventBus) : SendEventTask { private val eventBus: EventBus) : SendEventTask {
override suspend fun execute(params: SendEventTask.Params): String { override suspend fun execute(params: SendEventTask.Params): String {
try { 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 event = handleEncryption(params)
val localId = event.eventId!! val localId = event.eventId!!

View File

@ -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.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.database.model.PendingThreePidEntityFields 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.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 org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -28,7 +30,7 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { 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) { 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 <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm) if (oldVersion <= 4) migrateTo5(realm)
if (oldVersion <= 5) migrateTo6(realm) if (oldVersion <= 5) migrateTo6(realm)
if (oldVersion <= 6) migrateTo7(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -105,4 +108,18 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
.addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java) .addField(PreviewUrlCacheEntityFields.MXC_URL, String::class.java)
.addField(PreviewUrlCacheEntityFields.LAST_UPDATED_TIMESTAMP, Long::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")
}
} }

View File

@ -23,8 +23,7 @@ import io.realm.annotations.PrimaryKey
internal open class RoomEntity(@PrimaryKey var roomId: String = "", internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(), var chunks: RealmList<ChunkEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(), var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList()
var areAllMembersLoaded: Boolean = false
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name
@ -36,5 +35,14 @@ internal open class RoomEntity(@PrimaryKey var roomId: String = "",
membershipStr = value.name 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 companion object
} }

View File

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

View File

@ -17,12 +17,19 @@
package org.matrix.android.sdk.internal.session.room.membership package org.matrix.android.sdk.internal.session.room.membership
import com.zhuinden.monarchy.Monarchy 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.model.Membership
import org.matrix.android.sdk.api.session.room.send.SendState 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.mapper.toEntity
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntity 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.EventInsertType
import org.matrix.android.sdk.internal.database.model.RoomEntity 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.copyToRealmOrIgnore
import org.matrix.android.sdk.internal.database.query.getOrCreate import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.where 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.session.sync.SyncTokenStore
import org.matrix.android.sdk.internal.task.Task import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction import org.matrix.android.sdk.internal.util.awaitTransaction
import io.realm.Realm import java.util.concurrent.TimeUnit
import io.realm.kotlin.createObject
import org.greenrobot.eventbus.EventBus
import javax.inject.Inject import javax.inject.Inject
internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit> { internal interface LoadRoomMembersTask : Task<LoadRoomMembersTask.Params, Unit> {
@ -56,13 +61,40 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
) : LoadRoomMembersTask { ) : LoadRoomMembersTask {
override suspend fun execute(params: LoadRoomMembersTask.Params) { override suspend fun execute(params: LoadRoomMembersTask.Params) {
if (areAllMembersAlreadyLoaded(params.roomId)) { when (getRoomMembersLoadStatus(params.roomId)) {
return 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 lastToken = syncTokenStore.getLastToken()
val response = executeRequest<RoomMembersResponse>(eventBus) { val response = try {
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value) executeRequest<RoomMembersResponse>(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) insertInDb(response, params.roomId)
} }
@ -84,14 +116,23 @@ internal class DefaultLoadRoomMembersTask @Inject constructor(
} }
roomMemberEventHandler.handle(realm, roomId, roomMemberEvent) roomMemberEventHandler.handle(realm, roomId, roomMemberEvent)
} }
roomEntity.areAllMembersLoaded = true roomEntity.membersLoadStatus = RoomMembersLoadStatusType.LOADED
roomSummaryUpdater.update(realm, roomId, updateMembers = true) roomSummaryUpdater.update(realm, roomId, updateMembers = true)
} }
} }
private fun areAllMembersAlreadyLoaded(roomId: String): Boolean { private fun getRoomMembersLoadStatus(roomId: String): RoomMembersLoadStatusType {
return Realm.getInstance(monarchy.realmConfiguration).use { var result: RoomMembersLoadStatusType?
RoomEntity.where(it, roomId).findFirst()?.areAllMembersLoaded ?: false 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
} }
} }
} }

View File

@ -27,6 +27,7 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import org.matrix.android.sdk.api.MatrixCallback 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.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.events.model.EventType 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.findAllInRoomWithSendStates
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.database.query.whereRoomId 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.TaskExecutor
import org.matrix.android.sdk.internal.task.configureWith import org.matrix.android.sdk.internal.task.configureWith
import org.matrix.android.sdk.internal.util.Debouncer import org.matrix.android.sdk.internal.util.Debouncer
@ -81,7 +83,8 @@ internal class DefaultTimeline(
private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val hiddenReadReceipts: TimelineHiddenReadReceipts,
private val eventBus: EventBus, private val eventBus: EventBus,
private val eventDecryptor: TimelineEventDecryptor, private val eventDecryptor: TimelineEventDecryptor,
private val realmSessionProvider: RealmSessionProvider private val realmSessionProvider: RealmSessionProvider,
private val loadRoomMembersTask: LoadRoomMembersTask
) : Timeline, TimelineHiddenReadReceipts.Delegate { ) : Timeline, TimelineHiddenReadReceipts.Delegate {
data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>) data class OnNewTimelineEvents(val roomId: String, val eventIds: List<String>)
@ -184,6 +187,13 @@ internal class DefaultTimeline(
if (settings.shouldHandleHiddenReadReceipts()) { if (settings.shouldHandleHiddenReadReceipts()) {
hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this)
} }
loadRoomMembersTask
.configureWith(LoadRoomMembersTask.Params(roomId)) {
this.callback = NoOpMatrixCallback()
}
.executeBy(taskExecutor)
isReady.set(true) isReady.set(true)
} }
} }

View File

@ -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.model.TimelineEventEntityFields
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase 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 import org.matrix.android.sdk.internal.task.TaskExecutor
internal class DefaultTimelineService @AssistedInject constructor(@Assisted private val roomId: String, 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 paginationTask: PaginationTask,
private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask,
private val timelineEventMapper: TimelineEventMapper, private val timelineEventMapper: TimelineEventMapper,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
private val loadRoomMembersTask: LoadRoomMembersTask
) : TimelineService { ) : TimelineService {
@AssistedInject.Factory @AssistedInject.Factory
@ -73,7 +75,8 @@ internal class DefaultTimelineService @AssistedInject constructor(@Assisted priv
eventBus = eventBus, eventBus = eventBus,
eventDecryptor = eventDecryptor, eventDecryptor = eventDecryptor,
fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask,
realmSessionProvider = realmSessionProvider realmSessionProvider = realmSessionProvider,
loadRoomMembersTask = loadRoomMembersTask
) )
} }

View File

@ -161,7 +161,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # 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 ### 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 ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View File

@ -16,26 +16,6 @@
package im.vector.app.core.extensions 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" * Ex: "https://matrix.org/" -> "matrix.org"
*/ */

View File

@ -28,7 +28,7 @@ import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide 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 import im.vector.app.features.themes.ThemeUtils
class BottomSheetActionButton @JvmOverloads constructor( class BottomSheetActionButton @JvmOverloads constructor(
@ -36,7 +36,7 @@ class BottomSheetActionButton @JvmOverloads constructor(
attrs: AttributeSet? = null, attrs: AttributeSet? = null,
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
val views : ItemVerificationActionBinding val views: ViewBottomSheetActionButtonBinding
var title: String? = null var title: String? = null
set(value) { set(value) {
@ -97,8 +97,8 @@ class BottomSheetActionButton @JvmOverloads constructor(
} }
init { init {
inflate(context, R.layout.item_verification_action, this) inflate(context, R.layout.view_bottom_sheet_action_button, this)
views = ItemVerificationActionBinding.bind(this) views = ViewBottomSheetActionButtonBinding.bind(this)
context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) { context.withStyledAttributes(attrs, R.styleable.BottomSheetActionButton) {
title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: "" title = getString(R.styleable.BottomSheetActionButton_actionTitle) ?: ""

View File

@ -18,7 +18,7 @@ package im.vector.app.features.call
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.LinearLayout import android.widget.FrameLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.databinding.ViewCallControlsBinding import im.vector.app.databinding.ViewCallControlsBinding
@ -28,7 +28,7 @@ import org.webrtc.PeerConnection
class CallControlsView @JvmOverloads constructor( class CallControlsView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
private val views: ViewCallControlsBinding private val views: ViewCallControlsBinding

View File

@ -39,7 +39,6 @@ import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.pushers.PushersManager import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityHomeBinding import im.vector.app.databinding.ActivityHomeBinding
import im.vector.app.features.disclaimer.showDisclaimerDialog import im.vector.app.features.disclaimer.showDisclaimerDialog
import im.vector.app.features.matrixto.MatrixToBottomSheet import im.vector.app.features.matrixto.MatrixToBottomSheet
@ -166,8 +165,8 @@ class HomeActivity :
private fun handleIntent(intent: Intent?) { private fun handleIntent(intent: Intent?) {
intent?.dataString?.let { deepLink -> intent?.dataString?.let { deepLink ->
val resolvedLink = when { val resolvedLink = when {
deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink deepLink.startsWith(PermalinkService.MATRIX_TO_URL_BASE) -> deepLink
deepLink.startsWith(PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> { deepLink.startsWith(MATRIX_TO_CUSTOM_SCHEME_URL_BASE) -> {
// This is a bit ugly, but for now just convert to matrix.to link for compatibility // This is a bit ugly, but for now just convert to matrix.to link for compatibility
when { when {
deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length) deepLink.startsWith(USER_LINK_PREFIX) -> deepLink.substring(USER_LINK_PREFIX.length)
@ -177,7 +176,7 @@ class HomeActivity :
activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it) activeSessionHolder.getSafeActiveSession()?.permalinkService()?.createPermalink(it)
} }
} }
else -> null else -> return@let
} }
permalinkHandler.launch( permalinkHandler.launch(
@ -190,7 +189,11 @@ class HomeActivity :
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { isHandled -> .subscribe { isHandled ->
if (!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() .disposeOnDestroy()
@ -410,7 +413,8 @@ class HomeActivity :
} }
} }
private const val ROOM_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}room/" private const val MATRIX_TO_CUSTOM_SCHEME_URL_BASE = "element://"
private const val USER_LINK_PREFIX = "${PermalinkService.MATRIX_TO_CUSTOM_SCHEME_URL_BASE}user/" 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/"
} }
} }

View File

@ -31,7 +31,6 @@ import im.vector.app.R
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider 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.call.WebRtcPeerConnectionManager
import im.vector.app.features.command.CommandParser import im.vector.app.features.command.CommandParser
import im.vector.app.features.command.ParsedCommand import im.vector.app.features.command.ParsedCommand
@ -168,7 +167,6 @@ class RoomDetailViewModel @AssistedInject constructor(
observePowerLevel() observePowerLevel()
room.getRoomSummaryLive() room.getRoomSummaryLive()
room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback()) room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, NoOpMatrixCallback())
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId) session.onRoomDisplayed(initialState.roomId)
chatEffectManager.delegate = this chatEffectManager.delegate = this

View File

@ -57,7 +57,7 @@ class TimelineSettingsFactory @Inject constructor(
return map { return map {
EventTypeFilter( EventTypeFilter(
eventType = it, 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
) )
} }
} }

View File

@ -23,7 +23,7 @@ import android.widget.LinearLayout
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.databinding.ItemTimelineEventPollResultItemBinding import im.vector.app.databinding.ViewPollResultLineBinding
class PollResultLineView @JvmOverloads constructor( class PollResultLineView @JvmOverloads constructor(
context: Context, context: Context,
@ -31,7 +31,7 @@ class PollResultLineView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : LinearLayout(context, attrs, defStyleAttr) { ) : LinearLayout(context, attrs, defStyleAttr) {
private val views: ItemTimelineEventPollResultItemBinding private val views: ViewPollResultLineBinding
var label: String? = null var label: String? = null
set(value) { set(value) {
@ -60,8 +60,8 @@ class PollResultLineView @JvmOverloads constructor(
} }
init { init {
inflate(context, R.layout.item_timeline_event_poll_result_item, this) inflate(context, R.layout.view_poll_result_line, this)
views = ItemTimelineEventPollResultItemBinding.bind(this) views = ViewPollResultLineBinding.bind(this)
orientation = HORIZONTAL orientation = HORIZONTAL
context.withStyledAttributes(attrs, R.styleable.PollResultLineView) { context.withStyledAttributes(attrs, R.styleable.PollResultLineView) {

View File

@ -87,7 +87,12 @@ abstract class AbstractSSOLoginFragment<VB: ViewBinding> : AbstractLoginFragment
withState(loginViewModel) { state -> withState(loginViewModel) { state ->
if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) { if (state.loginMode.hasSso() && state.loginMode.ssoIdentityProviders().isNullOrEmpty()) {
// in this case we can prefetch (not other cases for privacy concerns) // 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) }
} }
} }
} }

View File

@ -360,6 +360,9 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), ToolbarCo
private const val EXTRA_CONFIG = "EXTRA_CONFIG" 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 { fun newIntent(context: Context, loginConfig: LoginConfig?): Intent {
return Intent(context, LoginActivity::class.java).apply { return Intent(context, LoginActivity::class.java).apply {
putExtra(EXTRA_CONFIG, loginConfig) putExtra(EXTRA_CONFIG, loginConfig)

View File

@ -193,7 +193,12 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
openInCustomTab(state.getSsoUrl(id)) loginViewModel.getSsoUrl(
redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
)
?.let { openInCustomTab(it) }
} }
} }
} else { } else {

View File

@ -76,8 +76,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders() views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener { views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) { override fun onProviderSelected(id: String?) {
val url = withState(loginViewModel) { it.getSsoUrl(id) } loginViewModel.getSsoUrl(
openInCustomTab(url) redirectUrl = LoginActivity.VECTOR_REDIRECT_URL,
deviceId = state.deviceId,
providerId = id
)
?.let { openInCustomTab(it) }
} }
} }
} }
@ -105,7 +109,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
private fun submit() = withState(loginViewModel) { state -> private fun submit() = withState(loginViewModel) { state ->
if (state.loginMode is LoginMode.Sso) { 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 { } else {
loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp)) loginViewModel.handle(LoginAction.UpdateSignMode(SignMode.SignUp))
} }

View File

@ -818,4 +818,12 @@ class LoginViewModel @AssistedInject constructor(
fun getInitialHomeServerUrl(): String? { fun getInitialHomeServerUrl(): String? {
return loginConfig?.homeServerUrl 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)
}
} }

View File

@ -22,10 +22,6 @@ import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.PersistState import com.airbnb.mvrx.PersistState
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized 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( data class LoginViewState(
val asyncLoginAction: Async<Unit> = Uninitialized, val asyncLoginAction: Async<Unit> = Uninitialized,
@ -69,27 +65,4 @@ data class LoginViewState(
fun isUserLogged(): Boolean { fun isUserLogged(): Boolean {
return asyncLoginAction is Success 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"
}
} }

View File

@ -33,14 +33,11 @@ import android.webkit.WebViewClient
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.appendParamToUrl
import im.vector.app.core.utils.AssetReader import im.vector.app.core.utils.AssetReader
import im.vector.app.databinding.FragmentLoginWebBinding import im.vector.app.databinding.FragmentLoginWebBinding
import im.vector.app.features.signout.soft.SoftLogoutAction import im.vector.app.features.signout.soft.SoftLogoutAction
import im.vector.app.features.signout.soft.SoftLogoutViewModel 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.api.auth.data.Credentials
import org.matrix.android.sdk.internal.di.MoshiProvider import org.matrix.android.sdk.internal.di.MoshiProvider
import timber.log.Timber import timber.log.Timber
@ -119,19 +116,7 @@ class LoginWebFragment @Inject constructor(
} }
private fun launchWebView(state: LoginViewState) { private fun launchWebView(state: LoginViewState) {
val url = buildString { val url = loginViewModel.getFallbackUrl(state.signMode == SignMode.SignIn, state.deviceId) ?: return
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)
}
}
views.loginWebWebView.loadUrl(url) views.loginWebWebView.loadUrl(url)

View File

@ -18,6 +18,8 @@ package im.vector.app.features.themes
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.TypedValue import android.util.TypedValue
import android.view.Menu import android.view.Menu
@ -39,10 +41,14 @@ object ThemeUtils {
const val APPLICATION_THEME_KEY = "APPLICATION_THEME_KEY" const val APPLICATION_THEME_KEY = "APPLICATION_THEME_KEY"
// the theme possible values // the theme possible values
private const val SYSTEM_THEME_VALUE = "system"
private const val THEME_DARK_VALUE = "dark" private const val THEME_DARK_VALUE = "dark"
private const val THEME_LIGHT_VALUE = "light" private const val THEME_LIGHT_VALUE = "light"
private const val THEME_BLACK_VALUE = "black" private const val THEME_BLACK_VALUE = "black"
// The default theme
private const val DEFAULT_THEME = SYSTEM_THEME_VALUE
private var currentTheme = AtomicReference<String>(null) private var currentTheme = AtomicReference<String>(null)
private val mColorByAttr = HashMap<Int, Int>() private val mColorByAttr = HashMap<Int, Int>()
@ -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 { fun isLightTheme(context: Context): Boolean {
return when (getApplicationTheme(context)) { val theme = getApplicationTheme(context)
THEME_LIGHT_VALUE -> true return theme == THEME_LIGHT_VALUE
else -> false || (theme == SYSTEM_THEME_VALUE && !isSystemDarkTheme(context.resources))
}
} }
/** /**
@ -73,11 +78,11 @@ object ThemeUtils {
val currentTheme = this.currentTheme.get() val currentTheme = this.currentTheme.get()
return if (currentTheme == null) { return if (currentTheme == null) {
val prefs = DefaultSharedPreferences.getInstance(context) 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") { if (themeFromPref == "status") {
// Migrate to light theme, which is the closest theme // Migrate to the default theme
themeFromPref = THEME_LIGHT_VALUE themeFromPref = DEFAULT_THEME
prefs.edit { putString(APPLICATION_THEME_KEY, THEME_LIGHT_VALUE) } prefs.edit { putString(APPLICATION_THEME_KEY, DEFAULT_THEME) }
} }
this.currentTheme.set(themeFromPref) this.currentTheme.set(themeFromPref)
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 * Update the application theme
* *
@ -93,11 +105,14 @@ object ThemeUtils {
*/ */
fun setApplicationTheme(context: Context, aTheme: String) { fun setApplicationTheme(context: Context, aTheme: String) {
currentTheme.set(aTheme) currentTheme.set(aTheme)
when (aTheme) { context.setTheme(
THEME_DARK_VALUE -> context.setTheme(R.style.AppTheme_Dark) when (aTheme) {
THEME_BLACK_VALUE -> context.setTheme(R.style.AppTheme_Black) SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(context.resources)) R.style.AppTheme_Dark else R.style.AppTheme_Light
else -> context.setTheme(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 // Clear the cache
mColorByAttr.clear() mColorByAttr.clear()
@ -110,6 +125,7 @@ object ThemeUtils {
*/ */
fun setActivityTheme(activity: Activity, otherThemes: ActivityOtherThemes) { fun setActivityTheme(activity: Activity, otherThemes: ActivityOtherThemes) {
when (getApplicationTheme(activity)) { when (getApplicationTheme(activity)) {
SYSTEM_THEME_VALUE -> if (isSystemDarkTheme(activity.resources)) activity.setTheme(otherThemes.dark)
THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark) THEME_DARK_VALUE -> activity.setTheme(otherThemes.dark)
THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black) THEME_BLACK_VALUE -> activity.setTheme(otherThemes.black)
} }
@ -117,40 +133,6 @@ object ThemeUtils {
mColorByAttr.clear() 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 * Translates color attributes to colors
* *

View File

@ -20,19 +20,19 @@ import android.content.Context
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.LinearLayout import android.widget.FrameLayout
import androidx.core.content.withStyledAttributes import androidx.core.content.withStyledAttributes
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setTextOrHide 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 import im.vector.app.features.themes.ThemeUtils
class SignOutBottomSheetActionButton @JvmOverloads constructor( class SignOutBottomSheetActionButton @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 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 var action: (() -> Unit)? = null
@ -67,8 +67,8 @@ class SignOutBottomSheetActionButton @JvmOverloads constructor(
} }
init { init {
inflate(context, R.layout.item_signout_action, this) inflate(context, R.layout.view_sign_out_bottom_sheet_action_button, this)
views = ItemSignoutActionBinding.bind(this) views = ViewSignOutBottomSheetActionButtonBinding.bind(this)
context.withStyledAttributes(attrs, R.styleable.SignOutBottomSheetActionButton) { context.withStyledAttributes(attrs, R.styleable.SignOutBottomSheetActionButton) {
title = getString(R.styleable.SignOutBottomSheetActionButton_actionTitle) ?: "" title = getString(R.styleable.SignOutBottomSheetActionButton_actionTitle) ?: ""

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/signedOutActionClickable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/actionIconImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_secure_backup"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/actionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/secure_backup_setup"
android:textColor="?riotx_text_secondary"
android:textSize="17sp" />
</LinearLayout>

View File

@ -24,10 +24,10 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_share"
tools:visibility="visible"
app:tint="?riotx_text_primary" app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix"
tools:src="@drawable/ic_share"
tools:visibility="visible" />
<TextView <TextView
@ -70,8 +70,8 @@
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_arrow_right"
app:tint="?riotx_text_primary" app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" /> tools:ignore="MissingPrefix"
tools:src="@drawable/ic_arrow_right" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/itemVerificationClickableZone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_bottom_sheet_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:minHeight="64dp"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/itemVerificationLeftIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="center"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_share"
tools:visibility="visible" />
<TextView
android:id="@+id/itemVerificationActionTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textColor="@color/riotx_accent"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/itemVerificationActionSubTitle"
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
app:layout_constraintStart_toEndOf="@+id/itemVerificationLeftIcon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
app:layout_goneMarginStart="0dp"
tools:text="@string/start_verification" />
<TextView
android:id="@+id/itemVerificationActionSubTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="?riotx_text_secondary"
android:textSize="12sp"
android:visibility="gone"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/itemVerificationActionIcon"
app:layout_constraintStart_toStartOf="@+id/itemVerificationActionTitle"
app:layout_constraintTop_toBottomOf="@+id/itemVerificationActionTitle"
tools:text="For maximum security, do this in person"
tools:visibility="visible" />
<ImageView
android:id="@+id/itemVerificationActionIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_arrow_right" />
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/ringingControls" android:id="@+id/ringingControls"
@ -23,8 +24,8 @@
android:focusable="true" android:focusable="true"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_call" android:src="@drawable/ic_call"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="@color/white"
app:tint="@color/white" /> tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView <ImageView
android:id="@+id/ringingControlDecline" android:id="@+id/ringingControlDecline"
@ -36,8 +37,8 @@
android:focusable="true" android:focusable="true"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_call_end" android:src="@drawable/ic_call_end"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="@color/white"
app:tint="@color/white" /> tools:ignore="MissingConstraints,MissingPrefix" />
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent" android:layout_width="match_parent"
@ -69,8 +70,8 @@
android:padding="10dp" android:padding="10dp"
android:src="@drawable/ic_home_bottom_chat" android:src="@drawable/ic_home_bottom_chat"
app:backgroundTint="?attr/riotx_background" app:backgroundTint="?attr/riotx_background"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="?attr/riotx_text_primary"
app:tint="?attr/riotx_text_primary" /> tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView <ImageView
android:id="@+id/muteIcon" android:id="@+id/muteIcon"
@ -82,10 +83,10 @@
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_microphone_off" android:src="@drawable/ic_microphone_off"
app:backgroundTint="?attr/riotx_background" app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_mute_microphone" tools:contentDescription="@string/a11y_mute_microphone"
tools:ignore="MissingConstraints,MissingPrefix" tools:ignore="MissingConstraints,MissingPrefix"
tools:src="@drawable/ic_microphone_on" tools:src="@drawable/ic_microphone_on" />
app:tint="?attr/riotx_text_primary" />
<ImageView <ImageView
android:id="@+id/iv_end_call" android:id="@+id/iv_end_call"
@ -97,8 +98,8 @@
android:focusable="true" android:focusable="true"
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_call_end" android:src="@drawable/ic_call_end"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="@color/white"
app:tint="@color/white" /> tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView <ImageView
android:id="@+id/videoToggleIcon" android:id="@+id/videoToggleIcon"
@ -110,9 +111,9 @@
android:padding="16dp" android:padding="16dp"
android:src="@drawable/ic_call_videocam_off_default" android:src="@drawable/ic_call_videocam_off_default"
app:backgroundTint="?attr/riotx_background" app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_stop_camera" tools:contentDescription="@string/a11y_stop_camera"
tools:ignore="MissingConstraints,MissingPrefix" tools:ignore="MissingConstraints,MissingPrefix" />
app:tint="?attr/riotx_text_primary" />
<ImageView <ImageView
android:id="@+id/iv_more" android:id="@+id/iv_more"
@ -125,8 +126,8 @@
android:padding="8dp" android:padding="8dp"
android:src="@drawable/ic_more_vertical" android:src="@drawable/ic_more_vertical"
app:backgroundTint="?attr/riotx_background" app:backgroundTint="?attr/riotx_background"
tools:ignore="MissingConstraints,MissingPrefix" app:tint="?attr/riotx_text_primary"
app:tint="?attr/riotx_text_primary" /> tools:ignore="MissingConstraints,MissingPrefix" />
<androidx.constraintlayout.helper.widget.Flow <androidx.constraintlayout.helper.widget.Flow
android:layout_width="match_parent" android:layout_width="match_parent"
@ -202,4 +203,4 @@
<!-- app:layout_constraintEnd_toEndOf="parent"--> <!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintTop_toTopOf="parent" />--> <!-- app:layout_constraintTop_toTopOf="parent" />-->
</FrameLayout> </merge>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<LinearLayout
android:id="@+id/signedOutActionClickable"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:minHeight="50dp"
android:orientation="horizontal"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="8dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/actionIconImageView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="16dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_secure_backup"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
<TextView
android:id="@+id/actionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/secure_backup_setup"
android:textColor="?riotx_text_secondary"
android:textSize="17sp" />
</LinearLayout>
</merge>

View File

@ -92,12 +92,14 @@
<!-- Theme --> <!-- Theme -->
<string-array name="theme_entries"> <string-array name="theme_entries">
<item>@string/system_theme</item>
<item>@string/light_theme</item> <item>@string/light_theme</item>
<item>@string/dark_theme</item> <item>@string/dark_theme</item>
<item>@string/black_them</item> <item>@string/black_them</item>
</string-array> </string-array>
<string-array name="theme_values"> <string-array name="theme_values">
<item>system</item>
<item>light</item> <item>light</item>
<item>dark</item> <item>dark</item>
<item>black</item> <item>black</item>

View File

@ -8,6 +8,7 @@
<string name="resources_script">Latn</string> <string name="resources_script">Latn</string>
<!-- theme --> <!-- theme -->
<string name="system_theme">System Default</string>
<string name="light_theme">Light Theme</string> <string name="light_theme">Light Theme</string>
<string name="dark_theme">Dark Theme</string> <string name="dark_theme">Dark Theme</string>
<string name="black_them">Black Theme</string> <string name="black_them">Black Theme</string>

View File

@ -13,7 +13,7 @@
app:fragment="im.vector.app.features.settings.locale.LocalePickerFragment" /> app:fragment="im.vector.app.features.settings.locale.LocalePickerFragment" />
<im.vector.app.core.preference.VectorListPreference <im.vector.app.core.preference.VectorListPreference
android:defaultValue="light" android:defaultValue="system"
android:entries="@array/theme_entries" android:entries="@array/theme_entries"
android:entryValues="@array/theme_values" android:entryValues="@array/theme_values"
android:key="APPLICATION_THEME_KEY" android:key="APPLICATION_THEME_KEY"