Merge pull request #4726 from vector-im/feature/bca/proper_encryption_state

Support misconfigured room encryption
This commit is contained in:
Benoit Marty 2022-01-11 16:47:36 +01:00 committed by GitHub
commit 67bdf4b226
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 413 additions and 99 deletions

1
changelog.d/4711.bugfix Normal file
View file

@ -0,0 +1 @@
Better handling of misconfigured room encryption

View file

@ -27,5 +27,8 @@ enum class RoomEncryptionTrustLevel {
Warning, Warning,
// All devices in the room are verified -> the app should display a green shield // All devices in the room are verified -> the app should display a green shield
Trusted Trusted,
// e2e is active but with an unsupported algorithm
E2EWithUnsupportedAlgorithm
} }

View file

@ -27,9 +27,12 @@ interface RoomCryptoService {
fun shouldEncryptForInvitedMembers(): Boolean fun shouldEncryptForInvitedMembers(): Boolean
/** /**
* Enable encryption of the room * Enable encryption of the room.
* @param Use force to ensure that this algorithm will be used. Otherwise this call
* will throw if encryption is already setup or if the algorithm is not supported. Only to
* be used by admins to fix misconfigured encryption.
*/ */
suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM) suspend fun enableEncryption(algorithm: String = MXCRYPTO_ALGORITHM_MEGOLM, force: Boolean = false)
/** /**
* Ensures all members of the room are loaded and outbound session keys are shared. * Ensures all members of the room are loaded and outbound session keys are shared.

View file

@ -0,0 +1,28 @@
/*
* Copyright 2021 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.session.room.model
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
sealed class RoomEncryptionAlgorithm {
abstract class SupportedAlgorithm(val alg: String) : RoomEncryptionAlgorithm()
object Megolm : SupportedAlgorithm(MXCRYPTO_ALGORITHM_MEGOLM)
data class UnsupportedAlgorithm(val name: String?) : RoomEncryptionAlgorithm()
}

View file

@ -62,7 +62,8 @@ data class RoomSummary(
val roomType: String? = null, val roomType: String? = null,
val spaceParents: List<SpaceParentInfo>? = null, val spaceParents: List<SpaceParentInfo>? = null,
val spaceChildren: List<SpaceChildInfo>? = null, val spaceChildren: List<SpaceChildInfo>? = null,
val flattenParentIds: List<String> = emptyList() val flattenParentIds: List<String> = emptyList(),
val roomEncryptionAlgorithm: RoomEncryptionAlgorithm? = null
) { ) {
val isVersioned: Boolean val isVersioned: Boolean

View file

@ -37,7 +37,6 @@ internal class CryptoSessionInfoProvider @Inject constructor(
fun isRoomEncrypted(roomId: String): Boolean { fun isRoomEncrypted(roomId: String): Boolean {
val encryptionEvent = monarchy.fetchCopied { realm -> val encryptionEvent = monarchy.fetchCopied { realm ->
EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION)
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"")
.isEmpty(EventEntityFields.STATE_KEY) .isEmpty(EventEntityFields.STATE_KEY)
.findFirst() .findFirst()
} }

View file

@ -177,7 +177,7 @@ internal class DefaultCryptoService @Inject constructor(
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
fun onStateEvent(roomId: String, event: Event) { fun onStateEvent(roomId: String, event: Event) {
when (event.getClearType()) { when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
@ -185,12 +185,15 @@ internal class DefaultCryptoService @Inject constructor(
} }
fun onLiveEvent(roomId: String, event: Event) { fun onLiveEvent(roomId: String, event: Event) {
when (event.getClearType()) { // handle state events
if (event.isStateEvent()) {
when (event.type) {
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event) EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event) EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event) EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
} }
} }
}
val gossipingBuffer = mutableListOf<Event>() val gossipingBuffer = mutableListOf<Event>()
@ -575,26 +578,31 @@ internal class DefaultCryptoService @Inject constructor(
// (for now at least. Maybe we should alert the user somehow?) // (for now at least. Maybe we should alert the user somehow?)
val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId) val existingAlgorithm = cryptoStore.getRoomAlgorithm(roomId)
if (!existingAlgorithm.isNullOrEmpty() && existingAlgorithm != algorithm) { if (existingAlgorithm == algorithm) {
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption event which requests a change of config in $roomId") // ignore
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Ignoring m.room.encryption for same alg ($algorithm) in $roomId")
return false return false
} }
val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm) val encryptingClass = MXCryptoAlgorithms.hasEncryptorClassForAlgorithm(algorithm)
// Always store even if not supported
cryptoStore.storeRoomAlgorithm(roomId, algorithm)
if (!encryptingClass) { if (!encryptingClass) {
Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm") Timber.tag(loggerTag.value).e("setEncryptionInRoom() : Unable to encrypt room $roomId with $algorithm")
return false return false
} }
cryptoStore.storeRoomAlgorithm(roomId, algorithm!!) val alg: IMXEncrypting? = when (algorithm) {
val alg: IMXEncrypting = when (algorithm) {
MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId) MXCRYPTO_ALGORITHM_MEGOLM -> megolmEncryptionFactory.create(roomId)
else -> olmEncryptionFactory.create(roomId) MXCRYPTO_ALGORITHM_OLM -> olmEncryptionFactory.create(roomId)
else -> null
} }
if (alg != null) {
roomEncryptorsStore.put(roomId, alg) roomEncryptorsStore.put(roomId, alg)
}
// if encryption was not previously enabled in this room, we will have been // if encryption was not previously enabled in this room, we will have been
// ignoring new device events for these users so far. We may well have // ignoring new device events for these users so far. We may well have
@ -927,6 +935,7 @@ internal class DefaultCryptoService @Inject constructor(
} }
private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) { private fun onRoomHistoryVisibilityEvent(roomId: String, event: Event) {
if (!event.isStateEvent()) return
val eventContent = event.content.toModel<RoomHistoryVisibilityContent>() val eventContent = event.content.toModel<RoomHistoryVisibilityContent>()
eventContent?.historyVisibility?.let { eventContent?.historyVisibility?.let {
cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED) cryptoStore.setShouldEncryptForInvitedMembers(roomId, it != RoomHistoryVisibility.JOINED)

View file

@ -27,7 +27,7 @@ data class EncryptionEventContent(
* Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'. * Required. The encryption algorithm to be used to encrypt messages sent in this room. Must be 'm.megolm.v1.aes-sha2'.
*/ */
@Json(name = "algorithm") @Json(name = "algorithm")
val algorithm: String, val algorithm: String?,
/** /**
* How long the session should be used before changing it. 604800000 (a week) is the recommended default. * How long the session should be used before changing it. 604800000 (a week) is the recommended default.

View file

@ -230,7 +230,7 @@ internal interface IMXCryptoStore {
* @param roomId the id of the room. * @param roomId the id of the room.
* @param algorithm the algorithm. * @param algorithm the algorithm.
*/ */
fun storeRoomAlgorithm(roomId: String, algorithm: String) fun storeRoomAlgorithm(roomId: String, algorithm: String?)
/** /**
* Provides the algorithm used in a dedicated room. * Provides the algorithm used in a dedicated room.

View file

@ -629,7 +629,7 @@ internal class RealmCryptoStore @Inject constructor(
} }
} }
override fun storeRoomAlgorithm(roomId: String, algorithm: String) { override fun storeRoomAlgorithm(roomId: String, algorithm: String?) {
doRealmTransaction(realmConfiguration) { doRealmTransaction(realmConfiguration) {
CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm CryptoRoomEntity.getOrCreate(it, roomId).algorithm = algorithm
} }

View file

@ -25,6 +25,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.VersioningState import org.matrix.android.sdk.api.session.room.model.VersioningState
import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import org.matrix.android.sdk.internal.database.model.ChunkEntityFields import org.matrix.android.sdk.internal.database.model.ChunkEntityFields
import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields import org.matrix.android.sdk.internal.database.model.CurrentStateEventEntityFields
import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields import org.matrix.android.sdk.internal.database.model.EditAggregatedSummaryEntityFields
@ -55,7 +56,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
) : RealmMigration { ) : RealmMigration {
companion object { companion object {
const val SESSION_STORE_SCHEMA_VERSION = 20L const val SESSION_STORE_SCHEMA_VERSION = 21L
} }
/** /**
@ -88,6 +89,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion <= 17) migrateTo18(realm) if (oldVersion <= 17) migrateTo18(realm)
if (oldVersion <= 18) migrateTo19(realm) if (oldVersion <= 18) migrateTo19(realm)
if (oldVersion <= 19) migrateTo20(realm) if (oldVersion <= 19) migrateTo20(realm)
if (oldVersion <= 20) migrateTo21(realm)
} }
private fun migrateTo1(realm: DynamicRealm) { private fun migrateTo1(realm: DynamicRealm) {
@ -395,6 +397,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private fun migrateTo20(realm: DynamicRealm) { private fun migrateTo20(realm: DynamicRealm) {
Timber.d("Step 19 -> 20") Timber.d("Step 19 -> 20")
realm.schema.get("ChunkEntity")?.apply { realm.schema.get("ChunkEntity")?.apply {
if (hasField("numberOfTimelineEvents")) { if (hasField("numberOfTimelineEvents")) {
removeField("numberOfTimelineEvents") removeField("numberOfTimelineEvents")
@ -414,4 +417,32 @@ internal class RealmSessionStoreMigration @Inject constructor(
} }
} }
} }
private fun migrateTo21(realm: DynamicRealm) {
Timber.d("Step 20 -> 21")
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.E2E_ALGORITHM, String::class.java)
?.transform { obj ->
val encryptionContentAdapter = MoshiProvider.providesMoshi().adapter(EncryptionEventContent::class.java)
val encryptionEvent = realm.where("CurrentStateEventEntity")
.equalTo(CurrentStateEventEntityFields.ROOM_ID, obj.getString(RoomSummaryEntityFields.ROOM_ID))
.equalTo(CurrentStateEventEntityFields.TYPE, EventType.STATE_ROOM_ENCRYPTION)
.findFirst()
val encryptionEventRoot = encryptionEvent?.getObject(CurrentStateEventEntityFields.ROOT.`$`)
val algorithm = encryptionEventRoot
?.getString(EventEntityFields.CONTENT)?.let {
encryptionContentAdapter.fromJson(it)?.algorithm
}
obj.setString(RoomSummaryEntityFields.E2E_ALGORITHM, algorithm)
obj.setBoolean(RoomSummaryEntityFields.IS_ENCRYPTED, encryptionEvent != null)
encryptionEventRoot?.getLong(EventEntityFields.ORIGIN_SERVER_TS)?.let {
obj.setLong(RoomSummaryEntityFields.ENCRYPTION_EVENT_TS, it)
}
}
}
} }

View file

@ -16,12 +16,15 @@
package org.matrix.android.sdk.internal.database.mapper package org.matrix.android.sdk.internal.database.mapper
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomJoinRules import org.matrix.android.sdk.api.session.room.model.RoomJoinRules
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo import org.matrix.android.sdk.api.session.room.model.SpaceParentInfo
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.typing.TypingUsersTracker import org.matrix.android.sdk.api.session.typing.TypingUsersTracker
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.model.presence.toUserPresence import org.matrix.android.sdk.internal.database.model.presence.toUserPresence
import javax.inject.Inject import javax.inject.Inject
@ -68,7 +71,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
isEncrypted = roomSummaryEntity.isEncrypted, isEncrypted = roomSummaryEntity.isEncrypted,
encryptionEventTs = roomSummaryEntity.encryptionEventTs, encryptionEventTs = roomSummaryEntity.encryptionEventTs,
breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex, breadcrumbsIndex = roomSummaryEntity.breadcrumbsIndex,
roomEncryptionTrustLevel = roomSummaryEntity.roomEncryptionTrustLevel, roomEncryptionTrustLevel = if (roomSummaryEntity.isEncrypted && roomSummaryEntity.e2eAlgorithm != MXCRYPTO_ALGORITHM_MEGOLM) {
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm
} else roomSummaryEntity.roomEncryptionTrustLevel,
inviterId = roomSummaryEntity.inviterId, inviterId = roomSummaryEntity.inviterId,
hasFailedSending = roomSummaryEntity.hasFailedSending, hasFailedSending = roomSummaryEntity.hasFailedSending,
roomType = roomSummaryEntity.roomType, roomType = roomSummaryEntity.roomType,
@ -99,7 +104,13 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC worldReadable = it.childSummaryEntity?.joinRules == RoomJoinRules.PUBLIC
) )
}, },
flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList(),
roomEncryptionAlgorithm = when (val alg = roomSummaryEntity.e2eAlgorithm) {
// I should probably use #hasEncryptorClassForAlgorithm but it says it supports
// OLM which is some legacy? Now only megolm allowed in rooms
MXCRYPTO_ALGORITHM_MEGOLM -> RoomEncryptionAlgorithm.Megolm
else -> RoomEncryptionAlgorithm.UnsupportedAlgorithm(alg)
}
) )
} }
} }

View file

@ -205,6 +205,11 @@ internal open class RoomSummaryEntity(
if (value != field) field = value if (value != field) field = value
} }
var e2eAlgorithm: String? = null
set(value) {
if (value != field) field = value
}
var encryptionEventTs: Long? = 0 var encryptionEventTs: Long? = 0
set(value) { set(value) {
if (value != field) field = value if (value != field) field = value

View file

@ -119,12 +119,12 @@ internal class DefaultRoom(override val roomId: String,
} }
} }
override suspend fun enableEncryption(algorithm: String) { override suspend fun enableEncryption(algorithm: String, force: Boolean) {
when { when {
isEncrypted() -> { (!force && isEncrypted() && encryptionAlgorithm() == MXCRYPTO_ALGORITHM_MEGOLM) -> {
throw IllegalStateException("Encryption is already enabled for this room") throw IllegalStateException("Encryption is already enabled for this room")
} }
algorithm != MXCRYPTO_ALGORITHM_MEGOLM -> { (!force && algorithm != MXCRYPTO_ALGORITHM_MEGOLM) -> {
throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported") throw InvalidParameterException("Only MXCRYPTO_ALGORITHM_MEGOLM algorithm is supported")
} }
else -> { else -> {

View file

@ -38,13 +38,11 @@ import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
import org.matrix.android.sdk.internal.crypto.EventDecryptor import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent
import org.matrix.android.sdk.internal.database.mapper.ContentMapper import org.matrix.android.sdk.internal.database.mapper.ContentMapper
import org.matrix.android.sdk.internal.database.mapper.asDomain import org.matrix.android.sdk.internal.database.mapper.asDomain
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.EventEntity
import org.matrix.android.sdk.internal.database.model.EventEntityFields
import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity import org.matrix.android.sdk.internal.database.model.GroupSummaryEntity
import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields import org.matrix.android.sdk.internal.database.model.RoomMemberSummaryEntityFields
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
@ -57,7 +55,6 @@ import org.matrix.android.sdk.internal.database.query.getOrCreate
import org.matrix.android.sdk.internal.database.query.getOrNull import org.matrix.android.sdk.internal.database.query.getOrNull
import org.matrix.android.sdk.internal.database.query.isEventRead import org.matrix.android.sdk.internal.database.query.isEventRead
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.whereType
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.extensions.clearWith import org.matrix.android.sdk.internal.extensions.clearWith
import org.matrix.android.sdk.internal.query.process import org.matrix.android.sdk.internal.query.process
@ -123,10 +120,8 @@ internal class RoomSummaryUpdater @Inject constructor(
Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]") Timber.v("## Space: Updating summary room [$roomId] roomType: [$roomType]")
// Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room // Don't use current state for this one as we are only interested in having MXCRYPTO_ALGORITHM_MEGOLM event in the room
val encryptionEvent = EventEntity.whereType(realm, roomId = roomId, type = EventType.STATE_ROOM_ENCRYPTION) val encryptionEvent = CurrentStateEventEntity.getOrNull(realm, roomId, type = EventType.STATE_ROOM_ENCRYPTION, stateKey = "")?.root
.contains(EventEntityFields.CONTENT, "\"algorithm\":\"$MXCRYPTO_ALGORITHM_MEGOLM\"") Timber.v("## CRYPTO: currentEncryptionEvent is $encryptionEvent")
.isNotNull(EventEntityFields.STATE_KEY)
.findFirst()
val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId) val latestPreviewableEvent = RoomSummaryEventsHelper.getLatestPreviewableEvent(realm, roomId)
@ -152,6 +147,11 @@ internal class RoomSummaryUpdater @Inject constructor(
.orEmpty() .orEmpty()
roomSummaryEntity.updateAliases(roomAliases) roomSummaryEntity.updateAliases(roomAliases)
roomSummaryEntity.isEncrypted = encryptionEvent != null roomSummaryEntity.isEncrypted = encryptionEvent != null
roomSummaryEntity.e2eAlgorithm = ContentMapper.map(encryptionEvent?.content)
?.toModel<EncryptionEventContent>()
?.algorithm
roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs roomSummaryEntity.encryptionEventTs = encryptionEvent?.originServerTs
if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) { if (roomSummaryEntity.membership == Membership.INVITE && inviterId != null) {

View file

@ -221,6 +221,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} }
val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it } val ageLocalTs = event.unsignedData?.age?.let { syncLocalTimestampMillis - it }
val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType) val eventEntity = event.toEntity(roomId, SendState.SYNCED, ageLocalTs).copyToRealmOrIgnore(realm, insertType)
Timber.v("## received state event ${event.type} and key ${event.stateKey}")
CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply { CurrentStateEventEntity.getOrCreate(realm, roomId, event.stateKey, event.type).apply {
// Timber.v("## Space state event: $eventEntity") // Timber.v("## Space state event: $eventEntity")
eventId = event.eventId eventId = event.eventId
@ -393,6 +394,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator) roomMemberEventHandler.handle(realm, roomEntity.roomId, event.stateKey, fixedContent, aggregator)
} }
} }
roomMemberContentsByUser.getOrPut(event.senderId) { roomMemberContentsByUser.getOrPut(event.senderId) {
// If we don't have any new state on this user, get it from db // If we don't have any new state on this user, get it from db
val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root val rootStateEvent = CurrentStateEventEntity.getOrNull(realm, roomId, event.senderId, EventType.STATE_ROOM_MEMBER)?.root

View file

@ -24,6 +24,7 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.epoxy.charsequence.EpoxyCharSequence
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
@ -38,7 +39,7 @@ import im.vector.app.features.themes.ThemeUtils
abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() { abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var text: String? = null var text: EpoxyCharSequence? = null
@EpoxyAttribute @EpoxyAttribute
var style: ItemStyle = ItemStyle.NORMAL_TEXT var style: ItemStyle = ItemStyle.NORMAL_TEXT
@ -56,7 +57,7 @@ abstract class GenericFooterItem : VectorEpoxyModel<GenericFooterItem.Holder>()
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.text.setTextOrHide(text) holder.text.setTextOrHide(text?.charSequence)
holder.text.typeface = style.toTypeFace() holder.text.typeface = style.toTypeFace()
holder.text.textSize = style.toTextSize() holder.text.textSize = style.toTextSize()
holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START holder.text.gravity = if (centered) Gravity.CENTER_HORIZONTAL else Gravity.START

View file

@ -25,6 +25,7 @@ import android.widget.LinearLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.italic import androidx.core.text.italic
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.error.ResourceLimitErrorFormatter import im.vector.app.core.error.ResourceLimitErrorFormatter
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
@ -73,6 +74,7 @@ class NotificationAreaView @JvmOverloads constructor(
is State.Default -> renderDefault() is State.Default -> renderDefault()
is State.Hidden -> renderHidden() is State.Hidden -> renderHidden()
is State.NoPermissionToPost -> renderNoPermissionToPost() is State.NoPermissionToPost -> renderNoPermissionToPost()
is State.UnsupportedAlgorithm -> renderUnsupportedAlgorithm(newState)
is State.Tombstone -> renderTombstone() is State.Tombstone -> renderTombstone()
is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState) is State.ResourceLimitExceededError -> renderResourceLimitExceededError(newState)
}.exhaustive }.exhaustive
@ -106,6 +108,24 @@ class NotificationAreaView @JvmOverloads constructor(
views.roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary)) views.roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary))
} }
private fun renderUnsupportedAlgorithm(e2eState: State.UnsupportedAlgorithm) {
visibility = View.VISIBLE
views.roomNotificationIcon.setImageResource(R.drawable.ic_warning_badge)
val text = if (e2eState.canRestore) {
R.string.room_unsupported_e2e_algorithm_as_admin
} else R.string.room_unsupported_e2e_algorithm
val message = span {
italic {
+resources.getString(text)
}
}
views.roomNotificationMessage.onClick {
delegate?.onMisconfiguredEncryptionClicked()
}
views.roomNotificationMessage.text = message
views.roomNotificationMessage.setTextColor(ThemeUtils.getColor(context, R.attr.vctr_content_secondary))
}
private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) { private fun renderResourceLimitExceededError(state: State.ResourceLimitExceededError) {
visibility = View.VISIBLE visibility = View.VISIBLE
val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context) val resourceLimitErrorFormatter = ResourceLimitErrorFormatter(context)
@ -163,6 +183,7 @@ class NotificationAreaView @JvmOverloads constructor(
// User can't post messages to room because his power level doesn't allow it. // User can't post messages to room because his power level doesn't allow it.
object NoPermissionToPost : State() object NoPermissionToPost : State()
data class UnsupportedAlgorithm(val canRestore: Boolean) : State()
// View will be Gone // View will be Gone
object Hidden : State() object Hidden : State()
@ -179,5 +200,6 @@ class NotificationAreaView @JvmOverloads constructor(
*/ */
interface Delegate { interface Delegate {
fun onTombstoneEventClicked() fun onTombstoneEventClicked()
fun onMisconfiguredEncryptionClicked()
} }
} }

View file

@ -61,6 +61,10 @@ class ShieldImageView @JvmOverloads constructor(
else R.drawable.ic_shield_trusted else R.drawable.ic_shield_trusted
) )
} }
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> {
contentDescription = context.getString(R.string.a11y_trust_level_trusted)
setImageResource(R.drawable.ic_warning_badge)
}
} }
} }
} }
@ -71,5 +75,6 @@ fun RoomEncryptionTrustLevel.toDrawableRes(): Int {
RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black RoomEncryptionTrustLevel.Default -> R.drawable.ic_shield_black
RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning RoomEncryptionTrustLevel.Warning -> R.drawable.ic_shield_warning
RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted RoomEncryptionTrustLevel.Trusted -> R.drawable.ic_shield_trusted
RoomEncryptionTrustLevel.E2EWithUnsupportedAlgorithm -> R.drawable.ic_warning_badge
} }
} }

View file

@ -18,6 +18,7 @@ package im.vector.app.features.devtools
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
@ -36,7 +37,7 @@ class RoomDevToolSendFormController @Inject constructor(
genericFooterItem { genericFooterItem {
id("topSpace") id("topSpace")
text("") text("".toEpoxyCharSequence())
} }
formEditTextItem { formEditTextItem {
id("event_type") id("event_type")

View file

@ -42,6 +42,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object MarkAllAsRead : RoomDetailAction() object MarkAllAsRead : RoomDetailAction()
data class DownloadOrOpen(val eventId: String, val senderId: String?, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction() data class DownloadOrOpen(val eventId: String, val senderId: String?, val messageFileContent: MessageWithAttachmentContent) : RoomDetailAction()
object JoinAndOpenReplacementRoom : RoomDetailAction() object JoinAndOpenReplacementRoom : RoomDetailAction()
object OnClickMisconfiguredEncryption : RoomDetailAction()
object AcceptInvite : RoomDetailAction() object AcceptInvite : RoomDetailAction()
object RejectInvite : RoomDetailAction() object RejectInvite : RoomDetailAction()

View file

@ -133,12 +133,14 @@ import im.vector.app.features.command.Command
import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity import im.vector.app.features.crypto.keysbackup.restore.KeysBackupRestoreActivity
import im.vector.app.features.crypto.verification.VerificationBottomSheet import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.composer.CanSendStatus
import im.vector.app.features.home.room.detail.composer.MessageComposerAction import im.vector.app.features.home.room.detail.composer.MessageComposerAction
import im.vector.app.features.home.room.detail.composer.MessageComposerView import im.vector.app.features.home.room.detail.composer.MessageComposerView
import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents import im.vector.app.features.home.room.detail.composer.MessageComposerViewEvents
import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel import im.vector.app.features.home.room.detail.composer.MessageComposerViewModel
import im.vector.app.features.home.room.detail.composer.MessageComposerViewState import im.vector.app.features.home.room.detail.composer.MessageComposerViewState
import im.vector.app.features.home.room.detail.composer.SendMode import im.vector.app.features.home.room.detail.composer.SendMode
import im.vector.app.features.home.room.detail.composer.boolean
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView
import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState import im.vector.app.features.home.room.detail.composer.voice.VoiceMessageRecorderView.RecordingUiState
import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet import im.vector.app.features.home.room.detail.readreceipts.DisplayReadReceiptsBottomSheet
@ -392,7 +394,7 @@ class RoomDetailFragment @Inject constructor(
} }
messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend -> messageComposerViewModel.onEach(MessageComposerViewState::sendMode, MessageComposerViewState::canSendMessage) { mode, canSend ->
if (!canSend) { if (!canSend.boolean()) {
return@onEach return@onEach
} }
when (mode) { when (mode) {
@ -459,7 +461,8 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it) is RoomDetailViewEvents.OpenRoom -> handleOpenRoom(it)
RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId) RoomDetailViewEvents.OpenInvitePeople -> navigator.openInviteUsersToRoom(requireContext(), roomDetailArgs.roomId)
RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show() RoomDetailViewEvents.OpenSetRoomAvatarDialog -> galleryOrCameraDialogHelper.show()
RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings() RoomDetailViewEvents.OpenRoomSettings -> handleOpenRoomSettings(RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS)
RoomDetailViewEvents.OpenRoomProfile -> handleOpenRoomSettings()
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item) navigator.openBigImageViewer(requireActivity(), it.view, item)
} }
@ -583,11 +586,11 @@ class RoomDetailFragment @Inject constructor(
) )
} }
private fun handleOpenRoomSettings() { private fun handleOpenRoomSettings(directAccess: Int? = null) {
navigator.openRoomProfile( navigator.openRoomProfile(
requireContext(), requireContext(),
roomDetailArgs.roomId, roomDetailArgs.roomId,
RoomProfileActivity.EXTRA_DIRECT_ACCESS_ROOM_SETTINGS directAccess
) )
} }
@ -947,6 +950,10 @@ class RoomDetailFragment @Inject constructor(
override fun onTombstoneEventClicked() { override fun onTombstoneEventClicked() {
roomDetailViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom) roomDetailViewModel.handle(RoomDetailAction.JoinAndOpenReplacementRoom)
} }
override fun onMisconfiguredEncryptionClicked() {
roomDetailViewModel.handle(RoomDetailAction.OnClickMisconfiguredEncryption)
}
} }
} }
@ -1268,7 +1275,7 @@ class RoomDetailFragment @Inject constructor(
val canSendMessage = withState(messageComposerViewModel) { val canSendMessage = withState(messageComposerViewModel) {
it.canSendMessage it.canSendMessage
} }
if (!canSendMessage) { if (!canSendMessage.boolean()) {
return false return false
} }
return when (model) { return when (model) {
@ -1446,10 +1453,18 @@ class RoomDetailFragment @Inject constructor(
views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState) views.voiceMessageRecorderView.render(messageComposerState.voiceRecordingUiState)
views.composerLayout.setRoomEncrypted(summary.isEncrypted) views.composerLayout.setRoomEncrypted(summary.isEncrypted)
// views.composerLayout.alwaysShowSendButton = false // views.composerLayout.alwaysShowSendButton = false
if (messageComposerState.canSendMessage) { when (messageComposerState.canSendMessage) {
views.notificationAreaView.render(NotificationAreaView.State.Hidden) CanSendStatus.Allowed -> {
} else { NotificationAreaView.State.Hidden
views.notificationAreaView.render(NotificationAreaView.State.NoPermissionToPost) }
CanSendStatus.NoPermission -> {
NotificationAreaView.State.NoPermissionToPost
}
is CanSendStatus.UnSupportedE2eAlgorithm -> {
NotificationAreaView.State.UnsupportedAlgorithm(mainState.isAllowedToSetupEncryption)
}
}.let {
views.notificationAreaView.render(it)
} }
} else { } else {
views.hideComposerViews() views.hideComposerViews()

View file

@ -48,6 +48,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
object OpenInvitePeople : RoomDetailViewEvents() object OpenInvitePeople : RoomDetailViewEvents()
object OpenSetRoomAvatarDialog : RoomDetailViewEvents() object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
object OpenRoomSettings : RoomDetailViewEvents() object OpenRoomSettings : RoomDetailViewEvents()
object OpenRoomProfile : RoomDetailViewEvents()
data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents() data class ShowRoomAvatarFullScreen(val matrixItem: MatrixItem?, val view: View?) : RoomDetailViewEvents()
object ShowWaitingView : RoomDetailViewEvents() object ShowWaitingView : RoomDetailViewEvents()

View file

@ -211,11 +211,13 @@ class RoomDetailViewModel @AssistedInject constructor(
val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId) val canInvite = PowerLevelsHelper(it).isUserAbleToInvite(session.myUserId)
val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId) val isAllowedToManageWidgets = session.widgetService().hasPermissionsToHandleWidgets(room.roomId)
val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE) val isAllowedToStartWebRTCCall = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.CALL_INVITE)
val isAllowedToSetupEncryption = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
setState { setState {
copy( copy(
canInvite = canInvite, canInvite = canInvite,
isAllowedToManageWidgets = isAllowedToManageWidgets, isAllowedToManageWidgets = isAllowedToManageWidgets,
isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall isAllowedToStartWebRTCCall = isAllowedToStartWebRTCCall,
isAllowedToSetupEncryption = isAllowedToSetupEncryption
) )
} }
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@ -309,6 +311,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom() is RoomDetailAction.JoinAndOpenReplacementRoom -> handleJoinAndOpenReplacementRoom()
is RoomDetailAction.OnClickMisconfiguredEncryption -> handleClickMisconfiguredE2E()
is RoomDetailAction.ResendMessage -> handleResendEvent(action) is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
@ -614,6 +617,12 @@ class RoomDetailViewModel @AssistedInject constructor(
} }
} }
private fun handleClickMisconfiguredE2E() = withState { state ->
if (state.isAllowedToSetupEncryption) {
_viewEvents.post(RoomDetailViewEvents.OpenRoomProfile)
}
}
private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled() private fun isIntegrationEnabled() = session.integrationManagerService().isIntegrationEnabled()
fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state -> fun isMenuItemVisible(@IdRes itemId: Int): Boolean = com.airbnb.mvrx.withState(this) { state ->

View file

@ -64,6 +64,7 @@ data class RoomDetailViewState(
val canInvite: Boolean = true, val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false, val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true, val isAllowedToStartWebRTCCall: Boolean = true,
val isAllowedToSetupEncryption: Boolean = true,
val hasFailedSending: Boolean = false, val hasFailedSending: Boolean = false,
val jitsiState: JitsiState = JitsiState() val jitsiState: JitsiState = JitsiState()
) : MavericksState { ) : MavericksState {

View file

@ -38,6 +38,7 @@ import im.vector.app.features.session.coroutineScope
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.voice.VoicePlayerHelper import im.vector.app.features.voice.VoicePlayerHelper
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -47,6 +48,7 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent import org.matrix.android.sdk.api.session.room.model.RoomAvatarContent
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
@ -55,6 +57,8 @@ import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent
import org.matrix.android.sdk.api.session.room.timeline.getRelationContent import org.matrix.android.sdk.api.session.room.timeline.getRelationContent
import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent import org.matrix.android.sdk.api.session.room.timeline.getTextEditableContent
import org.matrix.android.sdk.api.session.space.CreateSpaceParams import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber import timber.log.Timber
class MessageComposerViewModel @AssistedInject constructor( class MessageComposerViewModel @AssistedInject constructor(
@ -74,7 +78,7 @@ class MessageComposerViewModel @AssistedInject constructor(
init { init {
loadDraftIfAny() loadDraftIfAny()
observePowerLevel() observePowerLevelAndEncryption()
subscribeToStateInternal() subscribeToStateInternal()
} }
@ -137,11 +141,29 @@ class MessageComposerViewModel @AssistedInject constructor(
} }
} }
private fun observePowerLevel() { private fun observePowerLevelAndEncryption() {
PowerLevelsFlowFactory(room).createFlow() combine(
.setOnEach { PowerLevelsFlowFactory(room).createFlow(),
val canSendMessage = PowerLevelsHelper(it).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE) room.flow().liveRoomSummary().unwrap()
copy(canSendMessage = canSendMessage) ) { pl, sum ->
val canSendMessage = PowerLevelsHelper(pl).isUserAllowedToSend(session.myUserId, false, EventType.MESSAGE)
if (canSendMessage) {
val isE2E = sum.isEncrypted
if (isE2E) {
val roomEncryptionAlgorithm = sum.roomEncryptionAlgorithm
if (roomEncryptionAlgorithm is RoomEncryptionAlgorithm.UnsupportedAlgorithm) {
CanSendStatus.UnSupportedE2eAlgorithm(roomEncryptionAlgorithm.name)
} else {
CanSendStatus.Allowed
}
} else {
CanSendStatus.Allowed
}
} else {
CanSendStatus.NoPermission
}
}.setOnEach {
copy(canSendMessage = it)
} }
} }

View file

@ -43,9 +43,23 @@ sealed interface SendMode {
data class Voice(val text: String) : SendMode data class Voice(val text: String) : SendMode
} }
sealed interface CanSendStatus {
object Allowed : CanSendStatus
object NoPermission : CanSendStatus
data class UnSupportedE2eAlgorithm(val algorithm: String?) : CanSendStatus
}
fun CanSendStatus.boolean(): Boolean {
return when (this) {
CanSendStatus.Allowed -> true
CanSendStatus.NoPermission -> false
is CanSendStatus.UnSupportedE2eAlgorithm -> false
}
}
data class MessageComposerViewState( data class MessageComposerViewState(
val roomId: String, val roomId: String,
val canSendMessage: Boolean = true, val canSendMessage: CanSendStatus = CanSendStatus.Allowed,
val isSendButtonVisible: Boolean = false, val isSendButtonVisible: Boolean = false,
val sendMode: SendMode = SendMode.Regular("", false), val sendMode: SendMode = SendMode.Regular("", false),
val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle val voiceRecordingUiState: VoiceMessageRecorderView.RecordingUiState = VoiceMessageRecorderView.RecordingUiState.Idle
@ -60,8 +74,8 @@ data class MessageComposerViewState(
val isVoiceMessageIdle = !isVoiceRecording val isVoiceMessageIdle = !isVoiceRecording
val isComposerVisible = canSendMessage && !isVoiceRecording val isComposerVisible = canSendMessage.boolean() && !isVoiceRecording
val isVoiceMessageRecorderVisible = canSendMessage && !isSendButtonVisible val isVoiceMessageRecorderVisible = canSendMessage.boolean() && !isSendButtonVisible
@Suppress("UNUSED") // needed by mavericks @Suppress("UNUSED") // needed by mavericks
constructor(args: RoomDetailArgs) : this(roomId = args.roomId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId)

View file

@ -62,7 +62,7 @@ class ViewEditHistoryEpoxyController @Inject constructor(
is Fail -> { is Fail -> {
genericFooterItem { genericFooterItem {
id("failure") id("failure")
text(host.stringProvider.getString(R.string.unknown_error)) text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence())
} }
} }
is Success -> { is Success -> {

View file

@ -63,9 +63,9 @@ class EncryptionItemFactory @Inject constructor(
) )
shield = StatusTileTimelineItem.ShieldUIState.BLACK shield = StatusTileTimelineItem.ShieldUIState.BLACK
} else { } else {
title = stringProvider.getString(R.string.encryption_not_enabled) title = stringProvider.getString(R.string.encryption_misconfigured)
description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description) description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description)
shield = StatusTileTimelineItem.ShieldUIState.RED shield = StatusTileTimelineItem.ShieldUIState.ERROR
} }
return StatusTileTimelineItem_() return StatusTileTimelineItem_()
.attributes( .attributes(

View file

@ -57,6 +57,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
ShieldUIState.GREEN -> R.drawable.ic_shield_trusted ShieldUIState.GREEN -> R.drawable.ic_shield_trusted
ShieldUIState.BLACK -> R.drawable.ic_shield_black ShieldUIState.BLACK -> R.drawable.ic_shield_black
ShieldUIState.RED -> R.drawable.ic_shield_warning ShieldUIState.RED -> R.drawable.ic_shield_warning
ShieldUIState.ERROR -> R.drawable.ic_warning_badge
} }
holder.titleView.setCompoundDrawablesWithIntrinsicBounds( holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
@ -98,6 +99,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineIte
enum class ShieldUIState { enum class ShieldUIState {
BLACK, BLACK,
RED, RED,
GREEN GREEN,
ERROR
} }
} }

View file

@ -49,7 +49,7 @@ class ViewReactionsEpoxyController @Inject constructor(
is Fail -> { is Fail -> {
genericFooterItem { genericFooterItem {
id("failure") id("failure")
text(host.stringProvider.getString(R.string.unknown_error)) text(host.stringProvider.getString(R.string.unknown_error).toEpoxyCharSequence())
} }
} }
is Success -> { is Success -> {

View file

@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.widget
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericButtonItem
@ -40,7 +41,7 @@ class RoomWidgetsController @Inject constructor(
if (widgets.isEmpty()) { if (widgets.isEmpty()) {
genericFooterItem { genericFooterItem {
id("empty") id("empty")
text(host.stringProvider.getString(R.string.room_no_active_widgets)) text(host.stringProvider.getString(R.string.room_no_active_widgets).toEpoxyCharSequence())
} }
} else { } else {
widgets.forEach { widget -> widgets.forEach { widget ->

View file

@ -20,6 +20,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.EmojiCompatFontProvider import im.vector.app.EmojiCompatFontProvider
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import javax.inject.Inject import javax.inject.Inject
@ -52,13 +53,13 @@ class EmojiSearchResultController @Inject constructor(
// display 'Type something to find' // display 'Type something to find'
genericFooterItem { genericFooterItem {
id("type.query.item") id("type.query.item")
text(host.stringProvider.getString(R.string.reaction_search_type_hint)) text(host.stringProvider.getString(R.string.reaction_search_type_hint).toEpoxyCharSequence())
} }
} else { } else {
// Display no search Results // Display no search Results
genericFooterItem { genericFooterItem {
id("no.results.item") id("no.results.item")
text(host.stringProvider.getString(R.string.no_result_placeholder)) text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence())
} }
} }
} else { } else {

View file

@ -19,6 +19,7 @@ package im.vector.app.features.roommemberprofile
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
@ -95,11 +96,14 @@ class RoomMemberProfileController @Inject constructor(
private fun buildSecuritySection(state: RoomMemberProfileViewState) { private fun buildSecuritySection(state: RoomMemberProfileViewState) {
// Security // Security
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
val host = this val host = this
if (state.isRoomEncrypted) { if (state.isRoomEncrypted) {
if (state.userMXCrossSigningInfo != null) { if (!state.isAlgorithmSupported) {
// TODO find sensible message to display here
// For now we just remove the verify actions as well as the Security status
} else if (state.userMXCrossSigningInfo != null) {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
// Cross signing is enabled for this user // Cross signing is enabled for this user
if (state.userMXCrossSigningInfo.isTrusted()) { if (state.userMXCrossSigningInfo.isTrusted()) {
// User is trusted // User is trusted
@ -147,11 +151,13 @@ class RoomMemberProfileController @Inject constructor(
genericFooterItem { genericFooterItem {
id("verify_footer") id("verify_footer")
text(host.stringProvider.getString(R.string.room_profile_encrypted_subtitle)) text(host.stringProvider.getString(R.string.room_profile_encrypted_subtitle).toEpoxyCharSequence())
centered(false) centered(false)
} }
} }
} else { } else {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
buildProfileAction( buildProfileAction(
id = "learn_more", id = "learn_more",
title = stringProvider.getString(R.string.room_profile_section_security_learn_more), title = stringProvider.getString(R.string.room_profile_section_security_learn_more),
@ -162,9 +168,11 @@ class RoomMemberProfileController @Inject constructor(
) )
} }
} else { } else {
buildProfileSection(stringProvider.getString(R.string.room_profile_section_security))
genericFooterItem { genericFooterItem {
id("verify_footer_not_encrypted") id("verify_footer_not_encrypted")
text(host.stringProvider.getString(R.string.room_profile_not_encrypted_subtitle)) text(host.stringProvider.getString(R.string.room_profile_not_encrypted_subtitle).toEpoxyCharSequence())
centered(false) centered(false)
} }
} }

View file

@ -52,6 +52,7 @@ import org.matrix.android.sdk.api.session.profile.ProfileService
import org.matrix.android.sdk.api.session.room.Room import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
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.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomType import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.powerlevels.Role
@ -344,7 +345,15 @@ class RoomMemberProfileViewModel @AssistedInject constructor(
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
roomSummaryLive.execute { roomSummaryLive.execute {
copy(isRoomEncrypted = it.invoke()?.isEncrypted == true) val summary = it.invoke() ?: return@execute this
if (summary.isEncrypted) {
copy(
isRoomEncrypted = true,
isAlgorithmSupported = summary.roomEncryptionAlgorithm is RoomEncryptionAlgorithm.SupportedAlgorithm
)
} else {
copy(isRoomEncrypted = false)
}
} }
roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent -> roomSummaryLive.combine(powerLevelsContentLive) { roomSummary, powerLevelsContent ->
val roomName = roomSummary.toMatrixItem().getBestName() val roomName = roomSummary.toMatrixItem().getBestName()

View file

@ -33,6 +33,7 @@ data class RoomMemberProfileViewState(
val isMine: Boolean = false, val isMine: Boolean = false,
val isIgnored: Async<Boolean> = Uninitialized, val isIgnored: Async<Boolean> = Uninitialized,
val isRoomEncrypted: Boolean = false, val isRoomEncrypted: Boolean = false,
val isAlgorithmSupported: Boolean = true,
val powerLevelsContent: PowerLevelsContent? = null, val powerLevelsContent: PowerLevelsContent? = null,
val userPowerLevelString: Async<String> = Uninitialized, val userPowerLevelString: Async<String> = Uninitialized,
val userMatrixItem: Async<MatrixItem> = Uninitialized, val userMatrixItem: Async<MatrixItem> = Uninitialized,

View file

@ -97,7 +97,7 @@ class DeviceListEpoxyController @Inject constructor(private val stringProvider:
// Can this really happen? // Can this really happen?
genericFooterItem { genericFooterItem {
id("empty") id("empty")
text(host.stringProvider.getString(R.string.search_no_results)) text(host.stringProvider.getString(R.string.search_no_results).toEpoxyCharSequence())
} }
} else { } else {
// Build list of device with status // Build list of device with status

View file

@ -67,12 +67,12 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
// TODO FORMAT // TODO FORMAT
text(host.stringProvider.getString(R.string.verification_profile_device_verified_because, text(host.stringProvider.getString(R.string.verification_profile_device_verified_because,
data.userItem?.displayName ?: "", data.userItem?.displayName ?: "",
data.userItem?.id ?: "")) data.userItem?.id ?: "").toEpoxyCharSequence())
} else { } else {
// TODO what if mine // TODO what if mine
text(host.stringProvider.getString(R.string.verification_profile_device_new_signing, text(host.stringProvider.getString(R.string.verification_profile_device_new_signing,
data.userItem?.displayName ?: "", data.userItem?.displayName ?: "",
data.userItem?.id ?: "")) data.userItem?.id ?: "").toEpoxyCharSequence())
} }
} }
// text(stringProvider.getString(R.string.verification_profile_device_untrust_info)) // text(stringProvider.getString(R.string.verification_profile_device_untrust_info))
@ -98,7 +98,7 @@ class DeviceTrustInfoEpoxyController @Inject constructor(private val stringProvi
id("warn") id("warn")
centered(false) centered(false)
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
text(host.stringProvider.getString(R.string.verification_profile_device_untrust_info)) text(host.stringProvider.getString(R.string.verification_profile_device_untrust_info).toEpoxyCharSequence())
} }
bottomSheetVerificationActionItem { bottomSheetVerificationActionItem {

View file

@ -26,4 +26,5 @@ sealed class RoomProfileAction : VectorViewModelAction {
data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction() data class ChangeRoomNotificationState(val notificationState: RoomNotificationState) : RoomProfileAction()
object ShareRoomProfile : RoomProfileAction() object ShareRoomProfile : RoomProfileAction()
object CreateShortcut : RoomProfileAction() object CreateShortcut : RoomProfileAction()
object RestoreEncryptionState : RoomProfileAction()
} }

View file

@ -19,10 +19,12 @@ package im.vector.app.features.roomprofile
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.expandableTextItem import im.vector.app.core.epoxy.expandableTextItem
import im.vector.app.core.epoxy.profiles.buildProfileAction import im.vector.app.core.epoxy.profiles.buildProfileAction
import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericPositiveButtonItem import im.vector.app.core.ui.list.genericPositiveButtonItem
@ -30,7 +32,10 @@ import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import me.gujun.android.span.image
import me.gujun.android.span.span
import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel
import org.matrix.android.sdk.api.session.room.model.RoomEncryptionAlgorithm
import org.matrix.android.sdk.api.session.room.model.RoomSummary import org.matrix.android.sdk.api.session.room.model.RoomSummary
import javax.inject.Inject import javax.inject.Inject
@ -38,6 +43,7 @@ class RoomProfileController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val drawableProvider: DrawableProvider,
private val shortcutCreator: ShortcutCreator private val shortcutCreator: ShortcutCreator
) : TypedEpoxyController<RoomProfileViewState>() { ) : TypedEpoxyController<RoomProfileViewState>() {
@ -59,6 +65,7 @@ class RoomProfileController @Inject constructor(
fun onRoomDevToolsClicked() fun onRoomDevToolsClicked()
fun onUrlInTopicLongClicked(url: String) fun onUrlInTopicLongClicked(url: String)
fun doMigrateToVersion(newVersion: String) fun doMigrateToVersion(newVersion: String)
fun restoreEncryptionState()
} }
override fun buildModels(data: RoomProfileViewState?) { override fun buildModels(data: RoomProfileViewState?) {
@ -101,7 +108,7 @@ class RoomProfileController @Inject constructor(
data.recommendedRoomVersion != null) { data.recommendedRoomVersion != null) {
genericFooterItem { genericFooterItem {
id("version_warning") id("version_warning")
text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion)) text(host.stringProvider.getString(R.string.room_using_unstable_room_version, roomVersion).toEpoxyCharSequence())
textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) textColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
centered(false) centered(false)
} }
@ -113,15 +120,58 @@ class RoomProfileController @Inject constructor(
} }
} }
val learnMoreSubtitle = if (roomSummary.isEncrypted) { var encryptionMisconfigured = false
if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle else R.string.room_profile_encrypted_subtitle val e2eInfoText = if (roomSummary.isEncrypted) {
if (roomSummary.roomEncryptionAlgorithm is RoomEncryptionAlgorithm.SupportedAlgorithm) {
stringProvider.getString(
if (roomSummary.isDirect) R.string.direct_room_profile_encrypted_subtitle
else R.string.room_profile_encrypted_subtitle
)
} else { } else {
if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle else R.string.room_profile_not_encrypted_subtitle encryptionMisconfigured = true
buildString {
append(stringProvider.getString(R.string.encryption_has_been_misconfigured))
append(" ")
apply {
if (!data.canUpdateRoomState) {
append(stringProvider.getString(R.string.contact_admin_to_restore_encryption))
}
}
}
}
} else {
stringProvider.getString(
if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle
else R.string.room_profile_not_encrypted_subtitle
)
} }
genericFooterItem { genericFooterItem {
id("e2e info") id("e2e info")
centered(false) centered(false)
text(host.stringProvider.getString(learnMoreSubtitle)) text(
span {
apply {
if (encryptionMisconfigured) {
host.drawableProvider.getDrawable(R.drawable.ic_warning_badge)?.let {
image(it, "baseline")
}
+" "
}
}
+e2eInfoText
}.toEpoxyCharSequence()
)
}
if (encryptionMisconfigured && data.canUpdateRoomState) {
genericPositiveButtonItem {
id("restore_encryption")
text(host.stringProvider.getString(R.string.room_profile_section_restore_security))
iconRes(R.drawable.ic_shield_black_no_border)
buttonClickAction {
host.callback?.restoreEncryptionState()
}
}
} }
buildEncryptionAction(data.actionPermissions, roomSummary) buildEncryptionAction(data.actionPermissions, roomSummary)

View file

@ -121,6 +121,7 @@ class RoomProfileFragment @Inject constructor(
is RoomProfileViewEvents.Failure -> showFailure(it.throwable) is RoomProfileViewEvents.Failure -> showFailure(it.throwable)
is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink) is RoomProfileViewEvents.ShareRoomProfile -> onShareRoomProfile(it.permalink)
is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it) is RoomProfileViewEvents.OnShortcutReady -> addShortcut(it)
RoomProfileViewEvents.DismissLoading -> dismissLoadingDialog()
}.exhaustive }.exhaustive
} }
roomListQuickActionsSharedActionViewModel roomListQuickActionsSharedActionViewModel
@ -299,6 +300,10 @@ class RoomProfileFragment @Inject constructor(
roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings) roomProfileSharedActionViewModel.post(RoomProfileSharedAction.OpenRoomPermissionsSettings)
} }
override fun restoreEncryptionState() {
roomProfileViewModel.handle(RoomProfileAction.RestoreEncryptionState)
}
override fun onRoomIdClicked() { override fun onRoomIdClicked() {
copyToClipboard(requireContext(), roomProfileArgs.roomId) copyToClipboard(requireContext(), roomProfileArgs.roomId)
} }

View file

@ -24,6 +24,7 @@ import im.vector.app.core.platform.VectorViewEvents
*/ */
sealed class RoomProfileViewEvents : VectorViewEvents { sealed class RoomProfileViewEvents : VectorViewEvents {
data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents() data class Loading(val message: CharSequence? = null) : RoomProfileViewEvents()
object DismissLoading : RoomProfileViewEvents()
data class Failure(val throwable: Throwable) : RoomProfileViewEvents() data class Failure(val throwable: Throwable) : RoomProfileViewEvents()
data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents() data class ShareRoomProfile(val permalink: String) : RoomProfileViewEvents()

View file

@ -29,7 +29,10 @@ 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.features.home.ShortcutCreator import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.query.QueryStringValue import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -44,6 +47,7 @@ import org.matrix.android.sdk.flow.FlowRoom
import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.flow
import org.matrix.android.sdk.flow.mapOptional import org.matrix.android.sdk.flow.mapOptional
import org.matrix.android.sdk.flow.unwrap import org.matrix.android.sdk.flow.unwrap
import timber.log.Timber
class RoomProfileViewModel @AssistedInject constructor( class RoomProfileViewModel @AssistedInject constructor(
@Assisted private val initialState: RoomProfileViewState, @Assisted private val initialState: RoomProfileViewState,
@ -67,6 +71,19 @@ class RoomProfileViewModel @AssistedInject constructor(
observeRoomCreateContent(flowRoom) observeRoomCreateContent(flowRoom)
observeBannedRoomMembers(flowRoom) observeBannedRoomMembers(flowRoom)
observePermissions() observePermissions()
observePowerLevels()
}
private fun observePowerLevels() {
val powerLevelsContentLive = PowerLevelsFlowFactory(room).createFlow()
powerLevelsContentLive
.onEach {
val powerLevelsHelper = PowerLevelsHelper(it)
val canUpdateRoomState = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_ENCRYPTION)
setState {
copy(canUpdateRoomState = canUpdateRoomState)
}
}.launchIn(viewModelScope)
} }
private fun observeRoomCreateContent(flowRoom: FlowRoom) { private fun observeRoomCreateContent(flowRoom: FlowRoom) {
@ -119,6 +136,7 @@ class RoomProfileViewModel @AssistedInject constructor(
is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomProfileAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile() is RoomProfileAction.ShareRoomProfile -> handleShareRoomProfile()
RoomProfileAction.CreateShortcut -> handleCreateShortcut() RoomProfileAction.CreateShortcut -> handleCreateShortcut()
RoomProfileAction.RestoreEncryptionState -> restoreEncryptionState()
}.exhaustive }.exhaustive
} }
@ -182,4 +200,18 @@ class RoomProfileViewModel @AssistedInject constructor(
_viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink)) _viewEvents.post(RoomProfileViewEvents.ShareRoomProfile(permalink))
} }
} }
private fun restoreEncryptionState() {
_viewEvents.post(RoomProfileViewEvents.Loading())
session.coroutineScope.launch {
try {
room.enableEncryption(force = true)
} catch (failure: Throwable) {
Timber.e(failure, "Failed to restore encryption state in room ${room.roomId}")
_viewEvents.post(RoomProfileViewEvents.Failure(failure))
} finally {
_viewEvents.post(RoomProfileViewEvents.DismissLoading)
}
}
}
} }

View file

@ -34,7 +34,8 @@ data class RoomProfileViewState(
val isUsingUnstableRoomVersion: Boolean = false, val isUsingUnstableRoomVersion: Boolean = false,
val recommendedRoomVersion: String? = null, val recommendedRoomVersion: String? = null,
val canUpgradeRoom: Boolean = false, val canUpgradeRoom: Boolean = false,
val isTombstoned: Boolean = false val isTombstoned: Boolean = false,
val canUpdateRoomState: Boolean = false
) : MavericksState { ) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId) constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View file

@ -18,6 +18,7 @@ package im.vector.app.features.roomprofile.banned
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.epoxy.profiles.buildProfileSection import im.vector.app.core.epoxy.profiles.buildProfileSection
import im.vector.app.core.epoxy.profiles.profileMatrixItemWithProgress import im.vector.app.core.epoxy.profiles.profileMatrixItemWithProgress
@ -53,7 +54,7 @@ class RoomBannedMemberListController @Inject constructor(
genericFooterItem { genericFooterItem {
id("footer") id("footer")
text(quantityString) text(quantityString.toEpoxyCharSequence())
} }
} else { } else {
buildProfileSection(quantityString) buildProfileSection(quantityString)

View file

@ -18,6 +18,7 @@ package im.vector.app.features.roomprofile.settings.joinrule
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.ItemStyle
@ -49,7 +50,7 @@ class RoomJoinRuleAdvancedController @Inject constructor(
genericFooterItem { genericFooterItem {
id("header") id("header")
text(host.stringProvider.getString(R.string.room_settings_room_access_title)) text(host.stringProvider.getString(R.string.room_settings_room_access_title).toEpoxyCharSequence())
centered(false) centered(false)
style(ItemStyle.TITLE) style(ItemStyle.TITLE)
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
@ -57,7 +58,7 @@ class RoomJoinRuleAdvancedController @Inject constructor(
genericFooterItem { genericFooterItem {
id("desc") id("desc")
text(host.stringProvider.getString(R.string.decide_who_can_find_and_join)) text(host.stringProvider.getString(R.string.decide_who_can_find_and_join).toEpoxyCharSequence())
centered(false) centered(false)
} }

View file

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
@ -76,7 +77,7 @@ class ChooseRestrictedController @Inject constructor(
// when no filters // when no filters
genericFooterItem { genericFooterItem {
id("h1") id("h1")
text(host.stringProvider.getString(R.string.space_you_know_that_contains_this_room)) text(host.stringProvider.getString(R.string.space_you_know_that_contains_this_room).toEpoxyCharSequence())
centered(false) centered(false)
} }
@ -93,7 +94,7 @@ class ChooseRestrictedController @Inject constructor(
if (data.unknownRestricted.isNotEmpty()) { if (data.unknownRestricted.isNotEmpty()) {
genericFooterItem { genericFooterItem {
id("others") id("others")
text(host.stringProvider.getString(R.string.other_spaces_or_rooms_you_might_not_know)) text(host.stringProvider.getString(R.string.other_spaces_or_rooms_you_might_not_know).toEpoxyCharSequence())
centered(false) centered(false)
} }

View file

@ -293,7 +293,7 @@ class DeviceVerificationInfoBottomSheetController @Inject constructor(
genericFooterItem { genericFooterItem {
id("infoCrypto${info.deviceId}") id("infoCrypto${info.deviceId}")
text(host.stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info)) text(host.stringProvider.getString(R.string.settings_failed_to_get_crypto_device_info).toEpoxyCharSequence())
} }
info.deviceId?.let { addGenericDeviceManageActions(data, it) } info.deviceId?.let { addGenericDeviceManageActions(data, it) }

View file

@ -54,7 +54,7 @@ class AccountDataEpoxyController @Inject constructor(
is Fail -> { is Fail -> {
genericFooterItem { genericFooterItem {
id("fail") id("fail")
text(data.accountData.error.localizedMessage) text(data.accountData.error.localizedMessage?.toEpoxyCharSequence())
} }
} }
is Success -> { is Success -> {
@ -62,7 +62,7 @@ class AccountDataEpoxyController @Inject constructor(
if (dataList.isEmpty()) { if (dataList.isEmpty()) {
genericFooterItem { genericFooterItem {
id("noResults") id("noResults")
text(host.stringProvider.getString(R.string.no_result_placeholder)) text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence())
} }
} else { } else {
dataList.forEach { accountData -> dataList.forEach { accountData ->

View file

@ -18,6 +18,7 @@ package im.vector.app.features.settings.push
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import javax.inject.Inject import javax.inject.Inject
@ -34,7 +35,7 @@ class PushGateWayController @Inject constructor(
if (pushers.isEmpty()) { if (pushers.isEmpty()) {
genericFooterItem { genericFooterItem {
id("footer") id("footer")
text(host.stringProvider.getString(R.string.settings_push_gateway_no_pushers)) text(host.stringProvider.getString(R.string.settings_push_gateway_no_pushers).toEpoxyCharSequence())
} }
} else { } else {
pushers.forEach { pushers.forEach {
@ -50,7 +51,7 @@ class PushGateWayController @Inject constructor(
} ?: run { } ?: run {
genericFooterItem { genericFooterItem {
id("loading") id("loading")
text(host.stringProvider.getString(R.string.loading)) text(host.stringProvider.getString(R.string.loading).toEpoxyCharSequence())
} }
} }
} }

View file

@ -18,6 +18,7 @@ package im.vector.app.features.settings.push
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import javax.inject.Inject import javax.inject.Inject
@ -38,7 +39,7 @@ class PushRulesController @Inject constructor(
} ?: run { } ?: run {
genericFooterItem { genericFooterItem {
id("footer") id("footer")
text(host.stringProvider.getString(R.string.settings_push_rules_no_rules)) text(host.stringProvider.getString(R.string.settings_push_rules_no_rules).toEpoxyCharSequence())
} }
} }
} }

View file

@ -22,6 +22,7 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
@ -86,7 +87,7 @@ class ThreePidsSettingsController @Inject constructor(
is Fail -> { is Fail -> {
genericFooterItem { genericFooterItem {
id("fail") id("fail")
text(data.threePids.error.localizedMessage) text(data.threePids.error.localizedMessage?.toEpoxyCharSequence())
} }
} }
is Success -> { is Success -> {

View file

@ -19,6 +19,7 @@ package im.vector.app.features.spaces
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.app.R import im.vector.app.R
import im.vector.app.RoomGroupingMethod import im.vector.app.RoomGroupingMethod
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
@ -66,7 +67,7 @@ class SpaceSummaryController @Inject constructor(
if (!nonNullViewState.legacyGroups.isNullOrEmpty()) { if (!nonNullViewState.legacyGroups.isNullOrEmpty()) {
genericFooterItem { genericFooterItem {
id("legacy_space") id("legacy_space")
text(" ") text(" ".toEpoxyCharSequence())
} }
genericHeaderItem { genericHeaderItem {

View file

@ -43,12 +43,12 @@ class SpaceAdd3pidEpoxyController @Inject constructor(
genericFooterItem { genericFooterItem {
id("info_help_header") id("info_help_header")
style(ItemStyle.TITLE) style(ItemStyle.TITLE)
text(host.stringProvider.getString(R.string.create_spaces_invite_public_header)) text(host.stringProvider.getString(R.string.create_spaces_invite_public_header).toEpoxyCharSequence())
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
} }
genericFooterItem { genericFooterItem {
id("info_help_desc") id("info_help_desc")
text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: "")) text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: "").toEpoxyCharSequence())
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary))
} }

View file

@ -19,6 +19,7 @@ package im.vector.app.features.spaces.create
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle import im.vector.app.core.ui.list.ItemStyle
@ -45,7 +46,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
host.stringProvider.getString(R.string.create_spaces_room_public_header, data.name) host.stringProvider.getString(R.string.create_spaces_room_public_header, data.name)
} else { } else {
host.stringProvider.getString(R.string.create_spaces_room_private_header) host.stringProvider.getString(R.string.create_spaces_room_private_header)
} }.toEpoxyCharSequence()
) )
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
} }
@ -59,7 +60,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
} else { } else {
R.string.create_spaces_room_private_header_desc R.string.create_spaces_room_private_header_desc
} }
) ).toEpoxyCharSequence()
) )
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary)) textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary))
} }

View file

@ -20,6 +20,7 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.TextListener import im.vector.app.core.epoxy.TextListener
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
@ -61,7 +62,7 @@ class SpaceDetailEpoxyController @Inject constructor(
host.stringProvider.getString(R.string.create_spaces_details_public_header) host.stringProvider.getString(R.string.create_spaces_details_public_header)
} else { } else {
host.stringProvider.getString(R.string.create_spaces_details_private_header) host.stringProvider.getString(R.string.create_spaces_details_private_header)
} }.toEpoxyCharSequence()
) )
} }

View file

@ -21,6 +21,7 @@ import com.airbnb.epoxy.VisibilityState
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.charsequence.toEpoxyCharSequence
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
@ -74,7 +75,7 @@ class SpaceManageRoomsController @Inject constructor(
if (filteredResult.isEmpty()) { if (filteredResult.isEmpty()) {
genericFooterItem { genericFooterItem {
id("empty_result") id("empty_result")
text(host.stringProvider.getString(R.string.no_result_placeholder)) text(host.stringProvider.getString(R.string.no_result_placeholder).toEpoxyCharSequence())
} }
} else { } else {
filteredResult.forEach { childInfo -> filteredResult.forEach { childInfo ->

View file

@ -962,6 +962,8 @@
<string name="room_delete_unsent_messages">Delete unsent messages</string> <string name="room_delete_unsent_messages">Delete unsent messages</string>
<string name="room_message_file_not_found">File not found</string> <string name="room_message_file_not_found">File not found</string>
<string name="room_do_not_have_permission_to_post">You do not have permission to post to this room.</string> <string name="room_do_not_have_permission_to_post">You do not have permission to post to this room.</string>
<string name="room_unsupported_e2e_algorithm">Encryption has been misconfigured so you can\'t send messages. Please contact an admin to restore encryption to a valid state.</string>
<string name="room_unsupported_e2e_algorithm_as_admin">Encryption has been misconfigured so you can\'t send messages. Click to open settings.</string>
<plurals name="room_new_messages_notification"> <plurals name="room_new_messages_notification">
<item quantity="one">%d new message</item> <item quantity="one">%d new message</item>
<item quantity="other">%d new messages</item> <item quantity="other">%d new messages</item>
@ -2790,8 +2792,11 @@
<string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string> <string name="room_profile_not_encrypted_subtitle">Messages in this room are not end-to-end encrypted.</string>
<string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string> <string name="direct_room_profile_not_encrypted_subtitle">Messages here are not end-to-end encrypted.</string>
<string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string> <string name="room_profile_encrypted_subtitle">Messages in this room are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
<string name="encryption_has_been_misconfigured">Encryption has been misconfigured.</string>
<string name="contact_admin_to_restore_encryption">Please contact an admin to restore encryption to a valid state.</string>
<string name="direct_room_profile_encrypted_subtitle">Messages here are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string> <string name="direct_room_profile_encrypted_subtitle">Messages here are end-to-end encrypted.\n\nYour messages are secured with locks and only you and the recipient have the unique keys to unlock them.</string>
<string name="room_profile_section_security">Security</string> <string name="room_profile_section_security">Security</string>
<string name="room_profile_section_restore_security">Restore Encryption</string>
<string name="room_profile_section_security_learn_more">Learn more</string> <string name="room_profile_section_security_learn_more">Learn more</string>
<string name="room_profile_section_more">More</string> <string name="room_profile_section_more">More</string>
<string name="room_profile_section_admin">Admin Actions</string> <string name="room_profile_section_admin">Admin Actions</string>
@ -3052,6 +3057,7 @@
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string> <string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
<string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string> <string name="direct_room_encryption_enabled_tile_description">Messages in this room are end-to-end encrypted.</string>
<string name="encryption_not_enabled">Encryption not enabled</string> <string name="encryption_not_enabled">Encryption not enabled</string>
<string name="encryption_misconfigured">Encryption is misconfigured</string>
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string> <string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
<string name="room_created_summary_item">%s created and configured the room.</string> <string name="room_created_summary_item">%s created and configured the room.</string>
@ -3420,6 +3426,7 @@
<string name="a11y_trust_level_default">Default trust level</string> <string name="a11y_trust_level_default">Default trust level</string>
<string name="a11y_trust_level_warning">Warning trust level</string> <string name="a11y_trust_level_warning">Warning trust level</string>
<string name="a11y_trust_level_trusted">Trusted trust level</string> <string name="a11y_trust_level_trusted">Trusted trust level</string>
<string name="a11y_trust_level_misconfigured">Misconfigured trust level</string>
<string name="a11y_open_emoji_picker">Open Emoji picker</string> <string name="a11y_open_emoji_picker">Open Emoji picker</string>
<string name="a11y_close_emoji_picker">Close Emoji picker</string> <string name="a11y_close_emoji_picker">Close Emoji picker</string>
<string name="a11y_checked">Checked</string> <string name="a11y_checked">Checked</string>