mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-12-18 07:11:58 +03:00
Merge pull request #4726 from vector-im/feature/bca/proper_encryption_state
Support misconfigured room encryption
This commit is contained in:
commit
67bdf4b226
58 changed files with 413 additions and 99 deletions
1
changelog.d/4711.bugfix
Normal file
1
changelog.d/4711.bugfix
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Better handling of misconfigured room encryption
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,10 +185,13 @@ internal class DefaultCryptoService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onLiveEvent(roomId: String, event: Event) {
|
fun onLiveEvent(roomId: String, event: Event) {
|
||||||
when (event.getClearType()) {
|
// handle state events
|
||||||
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
if (event.isStateEvent()) {
|
||||||
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
when (event.type) {
|
||||||
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, event)
|
EventType.STATE_ROOM_ENCRYPTION -> onRoomEncryptionEvent(roomId, event)
|
||||||
|
EventType.STATE_ROOM_MEMBER -> onRoomMembershipEvent(roomId, event)
|
||||||
|
EventType.STATE_ROOM_HISTORY_VISIBILITY -> onRoomHistoryVisibilityEvent(roomId, 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
|
||||||
}
|
}
|
||||||
|
|
||||||
roomEncryptorsStore.put(roomId, alg)
|
if (alg != null) {
|
||||||
|
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)
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -119,15 +119,15 @@ 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 -> {
|
||||||
val params = SendStateTask.Params(
|
val params = SendStateTask.Params(
|
||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
stateKey = null,
|
stateKey = null,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,12 +141,30 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
|
private fun handleEnterQuoteMode(action: MessageComposerAction.EnterQuoteMode) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
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 {
|
} else {
|
||||||
if (roomSummary.isDirect) R.string.direct_room_profile_not_encrypted_subtitle else R.string.room_profile_not_encrypted_subtitle
|
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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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) }
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 -> {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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 & verify users in their profile.</string>
|
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & 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>
|
||||||
|
|
Loading…
Reference in a new issue