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)
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 🗣:
-

View file

@ -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()

View file

@ -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<TimelineEvent> {
fun sendTextMessage(room: Room, message: String, nbOfMessages: Int, timeout: Long = TestConstants.timeOutMillis): List<TimelineEvent> {
val timeline = room.createTimeline(null, TimelineSettings(10))
val sentEvents = ArrayList<TimelineEvent>(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<RegistrationResult> {
doSync<RegistrationResult>(timeout = 60_000) {
matrix.authenticationService
.getRegistrationWizard()
.createAccount(userName, password, null, it)
}
// Perform dummy step
val registrationResult = doSync<RegistrationResult> {
val registrationResult = doSync<RegistrationResult>(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

View file

@ -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<Session>) {
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)
}
}
}

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)
// 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<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
/**
* 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.
*/

View file

@ -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://"
}
/**

View file

@ -27,6 +27,7 @@ data class MessageStickerContent(
/**
* Set in local, not from server
*/
@Transient
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.
*/
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"

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.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<LoginFlowResult>): Cancelable {
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.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<SendEventTask.Params, String> {
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!!

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.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")
}
}

View file

@ -23,8 +23,7 @@ import io.realm.annotations.PrimaryKey
internal open class RoomEntity(@PrimaryKey var roomId: String = "",
var chunks: RealmList<ChunkEntity> = RealmList(),
var sendingTimelineEvents: RealmList<TimelineEventEntity> = RealmList(),
var areAllMembersLoaded: Boolean = false
var sendingTimelineEvents: RealmList<TimelineEventEntity> = 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
}

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
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<LoadRoomMembersTask.Params, Unit> {
@ -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<RoomMembersResponse>(eventBus) {
apiCall = roomAPI.getMembers(params.roomId, lastToken, null, params.excludeMembership?.value)
val response = try {
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)
}
@ -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
}
}
}

View file

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

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.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
)
}

View file

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

View file

@ -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"
*/

View file

@ -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) ?: ""

View file

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

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.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/"
}
}

View file

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

View file

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

View file

@ -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) {

View file

@ -87,7 +87,12 @@ abstract class AbstractSSOLoginFragment<VB: ViewBinding> : 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) }
}
}
}

View file

@ -360,6 +360,9 @@ open class LoginActivity : VectorBaseActivity<ActivityLoginBinding>(), 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)

View file

@ -193,7 +193,12 @@ class LoginFragment @Inject constructor() : AbstractSSOLoginFragment<FragmentLog
views.loginSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders
views.loginSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
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 {

View file

@ -76,8 +76,12 @@ class LoginSignUpSignInSelectionFragment @Inject constructor() : AbstractSSOLogi
views.loginSignupSigninSocialLoginButtons.ssoIdentityProviders = state.loginMode.ssoIdentityProviders()
views.loginSignupSigninSocialLoginButtons.listener = object : SocialLoginButtonsView.InteractionListener {
override fun onProviderSelected(id: String?) {
val url = withState(loginViewModel) { it.getSsoUrl(id) }
openInCustomTab(url)
loginViewModel.getSsoUrl(
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 ->
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))
}

View file

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

View file

@ -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<Unit> = 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"
}
}

View file

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

View file

@ -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<String>(null)
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 {
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
*

View file

@ -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) ?: ""

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_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" />
<TextView
@ -70,8 +70,8 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_arrow_right"
app:tint="?riotx_text_primary"
tools:ignore="MissingPrefix" />
tools:ignore="MissingPrefix"
tools:src="@drawable/ic_arrow_right" />
</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"?>
<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:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
tools:parentTag="android.widget.FrameLayout">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/ringingControls"
@ -23,8 +24,8 @@
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="@color/white" />
app:tint="@color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/ringingControlDecline"
@ -36,8 +37,8 @@
android:focusable="true"
android:padding="16dp"
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
android:layout_width="match_parent"
@ -69,8 +70,8 @@
android:padding="10dp"
android:src="@drawable/ic_home_bottom_chat"
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
android:id="@+id/muteIcon"
@ -82,10 +83,10 @@
android:padding="16dp"
android:src="@drawable/ic_microphone_off"
app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_mute_microphone"
tools:ignore="MissingConstraints,MissingPrefix"
tools:src="@drawable/ic_microphone_on"
app:tint="?attr/riotx_text_primary" />
tools:src="@drawable/ic_microphone_on" />
<ImageView
android:id="@+id/iv_end_call"
@ -97,8 +98,8 @@
android:focusable="true"
android:padding="16dp"
android:src="@drawable/ic_call_end"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="@color/white" />
app:tint="@color/white"
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/videoToggleIcon"
@ -110,9 +111,9 @@
android:padding="16dp"
android:src="@drawable/ic_call_videocam_off_default"
app:backgroundTint="?attr/riotx_background"
app:tint="?attr/riotx_text_primary"
tools:contentDescription="@string/a11y_stop_camera"
tools:ignore="MissingConstraints,MissingPrefix"
app:tint="?attr/riotx_text_primary" />
tools:ignore="MissingConstraints,MissingPrefix" />
<ImageView
android:id="@+id/iv_more"
@ -125,8 +126,8 @@
android:padding="8dp"
android:src="@drawable/ic_more_vertical"
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
android:layout_width="match_parent"
@ -202,4 +203,4 @@
<!-- app:layout_constraintEnd_toEndOf="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 -->
<string-array name="theme_entries">
<item>@string/system_theme</item>
<item>@string/light_theme</item>
<item>@string/dark_theme</item>
<item>@string/black_them</item>
</string-array>
<string-array name="theme_values">
<item>system</item>
<item>light</item>
<item>dark</item>
<item>black</item>

View file

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

View file

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