mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-23 01:45:52 +03:00
Merge branch 'develop' into improvement-957-catchup-indicator-on-invite
This commit is contained in:
commit
ef2abbfbd4
20 changed files with 604 additions and 96 deletions
|
@ -6,12 +6,14 @@ Features ✨:
|
||||||
- Cross-Signing | Verify new session from existing session (#1134)
|
- Cross-Signing | Verify new session from existing session (#1134)
|
||||||
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
|
||||||
|
|
||||||
|
|
||||||
Improvements 🙌:
|
Improvements 🙌:
|
||||||
- Verification DM / Handle concurrent .start after .ready (#794)
|
- Verification DM / Handle concurrent .start after .ready (#794)
|
||||||
- Cross-Signing | Update Shield Logic for DM (#963)
|
- Cross-Signing | Update Shield Logic for DM (#963)
|
||||||
- Cross-Signing | Complete security new session design update (#1135)
|
- Cross-Signing | Complete security new session design update (#1135)
|
||||||
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
|
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
|
||||||
- Cross-Signing | Gossip key backup recovery key (#1200)
|
- Cross-Signing | Gossip key backup recovery key (#1200)
|
||||||
|
- Show room encryption status as a bubble tile (#1078)
|
||||||
- UX/UI | Add indicator to home tab on invite (#957)
|
- UX/UI | Add indicator to home tab on invite (#957)
|
||||||
|
|
||||||
Bugfix 🐛:
|
Bugfix 🐛:
|
||||||
|
@ -20,6 +22,7 @@ Bugfix 🐛:
|
||||||
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
- RiotX can't restore cross signing keys saved by web in SSSS (#1174)
|
||||||
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
|
- Cross- Signing | After signin in new session, verification paper trail in DM is off (#1191)
|
||||||
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
|
- Failed to encrypt message in room (message stays in red), [thanks to pwr22] (#925)
|
||||||
|
- Cross-Signing | web <-> riotX After QR code scan, gossiping fails (#1210)
|
||||||
|
|
||||||
Translations 🗣:
|
Translations 🗣:
|
||||||
-
|
-
|
||||||
|
|
|
@ -282,7 +282,7 @@ class KeyShareTests : InstrumentedTest {
|
||||||
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
|
||||||
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
mTestHelper.retryPeriodicallyWithLatch(latch) {
|
||||||
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
|
||||||
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey != creationInfo.recoveryKey
|
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,9 @@ import dagger.Lazy
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.CryptoService
|
import im.vector.matrix.android.api.session.crypto.CryptoService
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
||||||
|
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
import im.vector.matrix.android.api.session.crypto.verification.CancelCode
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
import im.vector.matrix.android.api.session.crypto.verification.PendingVerificationRequest
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.QrCodeVerificationTransaction
|
||||||
|
@ -60,6 +63,7 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
|
||||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationAccept
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationCancel
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationDone
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationKey
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationMac
|
||||||
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
|
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
|
||||||
|
@ -109,6 +113,10 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
// map [sender : [transaction]]
|
// map [sender : [transaction]]
|
||||||
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
private val txMap = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||||
|
|
||||||
|
// we need to keep track of finished transaction
|
||||||
|
// It will be used for gossiping (to send request after request is completed and 'done' by other)
|
||||||
|
private val pastTransactions = HashMap<String, HashMap<String, DefaultVerificationTransaction>>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map [sender: [PendingVerificationRequest]]
|
* Map [sender: [PendingVerificationRequest]]
|
||||||
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
* For now we keep all requests (even terminated ones) during the lifetime of the app.
|
||||||
|
@ -137,6 +145,9 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
EventType.KEY_VERIFICATION_READY -> {
|
EventType.KEY_VERIFICATION_READY -> {
|
||||||
onReadyReceived(event)
|
onReadyReceived(event)
|
||||||
}
|
}
|
||||||
|
EventType.KEY_VERIFICATION_DONE -> {
|
||||||
|
onDoneReceived(event)
|
||||||
|
}
|
||||||
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
|
||||||
onRequestReceived(event)
|
onRequestReceived(event)
|
||||||
}
|
}
|
||||||
|
@ -778,6 +789,31 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onDoneReceived(event: Event) {
|
||||||
|
Timber.v("## onDoneReceived")
|
||||||
|
val doneReq = event.getClearContent().toModel<KeyVerificationDone>()?.asValidObject()
|
||||||
|
if (doneReq == null || event.senderId != userId) {
|
||||||
|
// ignore
|
||||||
|
Timber.e("## SAS Received invalid done request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only send gossiping request when the other sent us a done
|
||||||
|
// We can ask without checking too much thinks (like trust), because we will check validity of secret on reception
|
||||||
|
getExistingTransaction(userId, doneReq.transactionId)
|
||||||
|
?: getOldTransaction(userId, doneReq.transactionId)
|
||||||
|
?.let { vt ->
|
||||||
|
val otherDeviceId = vt.otherDeviceId
|
||||||
|
if (!crossSigningService.canCrossSign()) {
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||||
|
?: "*")))
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId
|
||||||
|
?: "*")))
|
||||||
|
}
|
||||||
|
outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun onRoomDoneReceived(event: Event) {
|
private fun onRoomDoneReceived(event: Event) {
|
||||||
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
|
||||||
?.copy(
|
?.copy(
|
||||||
|
@ -1003,7 +1039,11 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
|
|
||||||
private fun removeTransaction(otherUser: String, tid: String) {
|
private fun removeTransaction(otherUser: String, tid: String) {
|
||||||
synchronized(txMap) {
|
synchronized(txMap) {
|
||||||
txMap[otherUser]?.remove(tid)?.removeListener(this)
|
txMap[otherUser]?.remove(tid)?.also {
|
||||||
|
it.removeListener(this)
|
||||||
|
}
|
||||||
|
}?.let {
|
||||||
|
rememberOldTransaction(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1016,6 +1056,20 @@ internal class DefaultVerificationService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun rememberOldTransaction(tx: DefaultVerificationTransaction) {
|
||||||
|
synchronized(pastTransactions) {
|
||||||
|
pastTransactions.getOrPut(tx.otherUserId) { HashMap() }[tx.transactionId] = tx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOldTransaction(userId: String, tid: String?): DefaultVerificationTransaction? {
|
||||||
|
return tid?.let {
|
||||||
|
synchronized(pastTransactions) {
|
||||||
|
pastTransactions[userId]?.get(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? {
|
override fun beginKeyVerification(method: VerificationMethod, otherUserId: String, otherDeviceId: String, transactionId: String?): String? {
|
||||||
val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
|
val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
|
||||||
// should check if already one (and cancel it)
|
// should check if already one (and cancel it)
|
||||||
|
|
|
@ -17,9 +17,6 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
|
|
||||||
import im.vector.matrix.android.api.MatrixCallback
|
import im.vector.matrix.android.api.MatrixCallback
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
import im.vector.matrix.android.api.session.crypto.crosssigning.CrossSigningService
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
|
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.SELF_SIGNING_KEY_SSSS_NAME
|
|
||||||
import im.vector.matrix.android.api.session.crypto.crosssigning.USER_SIGNING_KEY_SSSS_NAME
|
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTransaction
|
||||||
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
import im.vector.matrix.android.api.session.crypto.verification.VerificationTxState
|
||||||
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
import im.vector.matrix.android.internal.crypto.IncomingGossipingRequestManager
|
||||||
|
@ -100,15 +97,15 @@ internal abstract class DefaultVerificationTransaction(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
transport.done(transactionId) {
|
|
||||||
if (otherUserId == userId && !crossSigningService.canCrossSign()) {
|
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
|
||||||
outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state = VerificationTxState.Verified
|
state = VerificationTxState.Verified
|
||||||
|
|
||||||
|
transport.done(transactionId) {
|
||||||
|
// if (otherUserId == userId && !crossSigningService.canCrossSign()) {
|
||||||
|
// outgoingGossipingRequestManager.sendSecretShareRequest(SELF_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
|
// outgoingGossipingRequestManager.sendSecretShareRequest(USER_SIGNING_KEY_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
|
// outgoingGossipingRequestManager.sendSecretShareRequest(KEYBACKUP_SECRET_SSSS_NAME, mapOf(userId to listOf(otherDeviceId ?: "*")))
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setDeviceVerified(userId: String, deviceId: String) {
|
private fun setDeviceVerified(userId: String, deviceId: String) {
|
||||||
|
|
|
@ -18,11 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification
|
||||||
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationInfoDone> {
|
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationInfoDone> {
|
||||||
|
|
||||||
override fun asValidObject(): ValidVerificationInfoDone? {
|
override fun asValidObject(): ValidVerificationInfoDone? {
|
||||||
if (transactionId.isNullOrEmpty()) {
|
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
|
||||||
return null
|
return ValidVerificationInfoDone(validTransactionId)
|
||||||
}
|
|
||||||
return ValidVerificationInfoDone
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal object ValidVerificationInfoDone
|
internal data class ValidVerificationInfoDone(val transactionId: String)
|
||||||
|
|
|
@ -47,9 +47,9 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVi
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
|
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
|
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
|
||||||
|
@ -373,7 +373,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
||||||
val localId: Long,
|
val localId: Long,
|
||||||
val eventId: String?,
|
val eventId: String?,
|
||||||
val eventModel: EpoxyModel<*>? = null,
|
val eventModel: EpoxyModel<*>? = null,
|
||||||
val mergedHeaderModel: MergedHeaderItem? = null,
|
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||||
val formattedDayModel: DaySeparatorItem? = null
|
val formattedDayModel: DaySeparatorItem? = null
|
||||||
) {
|
) {
|
||||||
fun shouldTriggerBuild(): Boolean {
|
fun shouldTriggerBuild(): Boolean {
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.riotx.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class EncryptionItemFactory @Inject constructor(
|
||||||
|
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||||
|
private val messageColorProvider: MessageColorProvider,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
|
private val informationDataFactory: MessageInformationDataFactory,
|
||||||
|
private val avatarSizeProvider: AvatarSizeProvider) {
|
||||||
|
|
||||||
|
fun create(event: TimelineEvent,
|
||||||
|
highlight: Boolean,
|
||||||
|
callback: TimelineEventController.Callback?): StatusTileTimelineItem? {
|
||||||
|
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
|
||||||
|
val informationData = informationDataFactory.create(event, null)
|
||||||
|
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||||
|
|
||||||
|
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
val title: String
|
||||||
|
val description: String
|
||||||
|
val shield: StatusTileTimelineItem.ShieldUIState
|
||||||
|
if (isSafeAlgorithm) {
|
||||||
|
title = stringProvider.getString(R.string.encryption_enabled)
|
||||||
|
description = stringProvider.getString(R.string.encryption_enabled_tile_description)
|
||||||
|
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
||||||
|
} else {
|
||||||
|
title = stringProvider.getString(R.string.encryption_not_enabled)
|
||||||
|
description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||||
|
shield = StatusTileTimelineItem.ShieldUIState.RED
|
||||||
|
}
|
||||||
|
return StatusTileTimelineItem_()
|
||||||
|
.attributes(
|
||||||
|
StatusTileTimelineItem.Attributes(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
shieldUIState = shield,
|
||||||
|
informationData = informationData,
|
||||||
|
avatarRenderer = attributes.avatarRenderer,
|
||||||
|
messageColorProvider = messageColorProvider,
|
||||||
|
emojiTypeFace = attributes.emojiTypeFace,
|
||||||
|
itemClickListener = attributes.itemClickListener,
|
||||||
|
itemLongClickListener = attributes.itemLongClickListener,
|
||||||
|
reactionPillCallback = attributes.reactionPillCallback,
|
||||||
|
readReceiptsCallback = attributes.readReceiptsCallback
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.highlighted(highlight)
|
||||||
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,16 +16,24 @@
|
||||||
|
|
||||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||||
|
|
||||||
|
import im.vector.matrix.android.api.session.events.model.EventType
|
||||||
|
import im.vector.matrix.android.api.session.events.model.toModel
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
|
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
|
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||||
import im.vector.riotx.core.di.ActiveSessionHolder
|
import im.vector.riotx.core.di.ActiveSessionHolder
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged
|
import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
|
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
|
||||||
|
@ -43,8 +51,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
requestModelBuild: () -> Unit)
|
requestModelBuild: () -> Unit)
|
||||||
: MergedHeaderItem? {
|
: BasedMergedItem<*>? {
|
||||||
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration()) {
|
||||||
|
// It's the first item before room.create
|
||||||
|
// Collapse all room configuration events
|
||||||
|
buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
|
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
||||||
|
@ -53,14 +65,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
} else {
|
} else {
|
||||||
var highlighted = false
|
var highlighted = false
|
||||||
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
|
||||||
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
|
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||||
mergedEvents.forEach { mergedEvent ->
|
mergedEvents.forEach { mergedEvent ->
|
||||||
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||||
highlighted = true
|
highlighted = true
|
||||||
}
|
}
|
||||||
val senderAvatar = mergedEvent.senderAvatar
|
val senderAvatar = mergedEvent.senderAvatar
|
||||||
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
||||||
val data = MergedHeaderItem.Data(
|
val data = BasedMergedItem.Data(
|
||||||
userId = mergedEvent.root.senderId ?: "",
|
userId = mergedEvent.root.senderId ?: "",
|
||||||
avatarUrl = senderAvatar,
|
avatarUrl = senderAvatar,
|
||||||
memberName = senderName,
|
memberName = senderName,
|
||||||
|
@ -82,7 +94,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
collapsedEventIds.removeAll(mergedEventIds)
|
collapsedEventIds.removeAll(mergedEventIds)
|
||||||
}
|
}
|
||||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||||
val attributes = MergedHeaderItem.Attributes(
|
val attributes = MergedMembershipEventsItem.Attributes(
|
||||||
isCollapsed = isCollapsed,
|
isCollapsed = isCollapsed,
|
||||||
mergeData = mergedData,
|
mergeData = mergedData,
|
||||||
avatarRenderer = avatarRenderer,
|
avatarRenderer = avatarRenderer,
|
||||||
|
@ -92,7 +104,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
},
|
},
|
||||||
readReceiptsCallback = callback
|
readReceiptsCallback = callback
|
||||||
)
|
)
|
||||||
MergedHeaderItem_()
|
MergedMembershipEventsItem_()
|
||||||
.id(mergeId)
|
.id(mergeId)
|
||||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
.highlighted(isCollapsed && highlighted)
|
.highlighted(isCollapsed && highlighted)
|
||||||
|
@ -104,6 +116,81 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun buildRoomCreationMergedSummary(currentPosition: Int,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
event: TimelineEvent,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
|
requestModelBuild: () -> Unit,
|
||||||
|
callback: TimelineEventController.Callback?): MergedRoomCreationItem_? {
|
||||||
|
var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null
|
||||||
|
var tmpPos = currentPosition - 1
|
||||||
|
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
|
||||||
|
var hasEncryption = false
|
||||||
|
var encryptionAlgorithm: String? = null
|
||||||
|
while (prevEvent != null && prevEvent.isRoomConfiguration()) {
|
||||||
|
if (prevEvent.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION) {
|
||||||
|
hasEncryption = true
|
||||||
|
encryptionAlgorithm = prevEvent.root.getClearContent()?.toModel<EncryptionEventContent>()?.algorithm
|
||||||
|
}
|
||||||
|
mergedEvents.add(prevEvent)
|
||||||
|
tmpPos--
|
||||||
|
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
|
||||||
|
}
|
||||||
|
return if (mergedEvents.size > 2) {
|
||||||
|
var highlighted = false
|
||||||
|
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||||
|
mergedEvents.reversed()
|
||||||
|
.forEach { mergedEvent ->
|
||||||
|
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
|
||||||
|
highlighted = true
|
||||||
|
}
|
||||||
|
val senderAvatar = mergedEvent.senderAvatar
|
||||||
|
val senderName = mergedEvent.getDisambiguatedDisplayName()
|
||||||
|
val data = BasedMergedItem.Data(
|
||||||
|
userId = mergedEvent.root.senderId ?: "",
|
||||||
|
avatarUrl = senderAvatar,
|
||||||
|
memberName = senderName,
|
||||||
|
localId = mergedEvent.localId,
|
||||||
|
eventId = mergedEvent.root.eventId ?: ""
|
||||||
|
)
|
||||||
|
mergedData.add(data)
|
||||||
|
}
|
||||||
|
val mergedEventIds = mergedEvents.map { it.localId }
|
||||||
|
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
||||||
|
// => handle case where paginating from mergeable events and we get more
|
||||||
|
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||||
|
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
|
||||||
|
?: true
|
||||||
|
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
|
||||||
|
if (isCollapsed) {
|
||||||
|
collapsedEventIds.addAll(mergedEventIds)
|
||||||
|
} else {
|
||||||
|
collapsedEventIds.removeAll(mergedEventIds)
|
||||||
|
}
|
||||||
|
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||||
|
val attributes = MergedRoomCreationItem.Attributes(
|
||||||
|
isCollapsed = isCollapsed,
|
||||||
|
mergeData = mergedData,
|
||||||
|
avatarRenderer = avatarRenderer,
|
||||||
|
onCollapsedStateChanged = {
|
||||||
|
mergeItemCollapseStates[event.localId] = it
|
||||||
|
requestModelBuild()
|
||||||
|
},
|
||||||
|
hasEncryptionEvent = hasEncryption,
|
||||||
|
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||||
|
readReceiptsCallback = callback
|
||||||
|
)
|
||||||
|
MergedRoomCreationItem_()
|
||||||
|
.id(mergeId)
|
||||||
|
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||||
|
.highlighted(isCollapsed && highlighted)
|
||||||
|
.attributes(attributes)
|
||||||
|
.also {
|
||||||
|
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
}
|
||||||
|
|
||||||
fun isCollapsed(localId: Long): Boolean {
|
fun isCollapsed(localId: Long): Boolean {
|
||||||
return collapsedEventIds.contains(localId)
|
return collapsedEventIds.contains(localId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
private val encryptedItemFactory: EncryptedItemFactory,
|
private val encryptedItemFactory: EncryptedItemFactory,
|
||||||
private val noticeItemFactory: NoticeItemFactory,
|
private val noticeItemFactory: NoticeItemFactory,
|
||||||
private val defaultItemFactory: DefaultItemFactory,
|
private val defaultItemFactory: DefaultItemFactory,
|
||||||
|
private val encryptionItemFactory: EncryptionItemFactory,
|
||||||
private val roomCreateItemFactory: RoomCreateItemFactory,
|
private val roomCreateItemFactory: RoomCreateItemFactory,
|
||||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||||
|
@ -57,8 +58,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
EventType.CALL_ANSWER,
|
EventType.CALL_ANSWER,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
EventType.REDACTION,
|
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
|
||||||
EventType.STATE_ROOM_ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
|
EventType.STATE_ROOM_ENCRYPTION -> {
|
||||||
|
encryptionItemFactory.create(event, highlight, callback)
|
||||||
|
}
|
||||||
// State room create
|
// State room create
|
||||||
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
|
||||||
// Crypto
|
// Crypto
|
||||||
|
|
|
@ -25,15 +25,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageRelationCo
|
||||||
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
|
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
|
||||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||||
import im.vector.matrix.android.internal.session.room.VerificationState
|
import im.vector.matrix.android.internal.session.room.VerificationState
|
||||||
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
import im.vector.riotx.core.epoxy.VectorEpoxyModel
|
||||||
|
import im.vector.riotx.core.resources.StringProvider
|
||||||
import im.vector.riotx.core.resources.UserPreferencesProvider
|
import im.vector.riotx.core.resources.UserPreferencesProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
|
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
|
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,6 +50,7 @@ class VerificationItemFactory @Inject constructor(
|
||||||
private val avatarSizeProvider: AvatarSizeProvider,
|
private val avatarSizeProvider: AvatarSizeProvider,
|
||||||
private val noticeItemFactory: NoticeItemFactory,
|
private val noticeItemFactory: NoticeItemFactory,
|
||||||
private val userPreferencesProvider: UserPreferencesProvider,
|
private val userPreferencesProvider: UserPreferencesProvider,
|
||||||
|
private val stringProvider: StringProvider,
|
||||||
private val session: Session
|
private val session: Session
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -88,12 +91,12 @@ class VerificationItemFactory @Inject constructor(
|
||||||
CancelCode.MismatchedKeys,
|
CancelCode.MismatchedKeys,
|
||||||
CancelCode.MismatchedSas -> {
|
CancelCode.MismatchedSas -> {
|
||||||
// We should display these bad conclusions
|
// We should display these bad conclusions
|
||||||
return VerificationRequestConclusionItem_()
|
return StatusTileTimelineItem_()
|
||||||
.attributes(
|
.attributes(
|
||||||
VerificationRequestConclusionItem.Attributes(
|
StatusTileTimelineItem.Attributes(
|
||||||
toUserId = informationData.senderId,
|
title = stringProvider.getString(R.string.verification_conclusion_warning),
|
||||||
toUserName = informationData.memberName.toString(),
|
description = "${informationData.memberName} (${informationData.senderId})",
|
||||||
isPositive = false,
|
shieldUIState = StatusTileTimelineItem.ShieldUIState.RED,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
avatarRenderer = attributes.avatarRenderer,
|
avatarRenderer = attributes.avatarRenderer,
|
||||||
messageColorProvider = messageColorProvider,
|
messageColorProvider = messageColorProvider,
|
||||||
|
@ -121,12 +124,12 @@ class VerificationItemFactory @Inject constructor(
|
||||||
// We only display the done sent by the other user, the done send by me is ignored
|
// We only display the done sent by the other user, the done send by me is ignored
|
||||||
return ignoredConclusion(event, highlight, callback)
|
return ignoredConclusion(event, highlight, callback)
|
||||||
}
|
}
|
||||||
return VerificationRequestConclusionItem_()
|
return StatusTileTimelineItem_()
|
||||||
.attributes(
|
.attributes(
|
||||||
VerificationRequestConclusionItem.Attributes(
|
StatusTileTimelineItem.Attributes(
|
||||||
toUserId = informationData.senderId,
|
title = stringProvider.getString(R.string.sas_verified),
|
||||||
toUserName = informationData.memberName.toString(),
|
description = "${informationData.memberName} (${informationData.senderId})",
|
||||||
isPositive = true,
|
shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN,
|
||||||
informationData = informationData,
|
informationData = informationData,
|
||||||
avatarRenderer = attributes.avatarRenderer,
|
avatarRenderer = attributes.avatarRenderer,
|
||||||
messageColorProvider = messageColorProvider,
|
messageColorProvider = messageColorProvider,
|
||||||
|
|
|
@ -50,6 +50,18 @@ fun TimelineEvent.canBeMerged(): Boolean {
|
||||||
return root.getClearType() == EventType.STATE_ROOM_MEMBER
|
return root.getClearType() == EventType.STATE_ROOM_MEMBER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.isRoomConfiguration(): Boolean {
|
||||||
|
return when (root.getClearType()) {
|
||||||
|
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||||
|
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||||
|
EventType.STATE_ROOM_JOIN_RULES,
|
||||||
|
EventType.STATE_ROOM_MEMBER,
|
||||||
|
EventType.STATE_ROOM_NAME,
|
||||||
|
EventType.STATE_ROOM_ENCRYPTION -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
|
||||||
if (index >= size - 1) {
|
if (index >= size - 1) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import im.vector.matrix.android.api.util.MatrixItem
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
|
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
|
||||||
|
|
||||||
|
abstract val attributes: Attributes
|
||||||
|
|
||||||
|
override fun bind(holder: H) {
|
||||||
|
super.bind(holder)
|
||||||
|
holder.expandView.setOnClickListener {
|
||||||
|
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
|
||||||
|
}
|
||||||
|
if (attributes.isCollapsed) {
|
||||||
|
holder.separatorView.visibility = View.GONE
|
||||||
|
holder.expandView.setText(R.string.merged_events_expand)
|
||||||
|
} else {
|
||||||
|
holder.separatorView.visibility = View.VISIBLE
|
||||||
|
holder.expandView.setText(R.string.merged_events_collapse)
|
||||||
|
}
|
||||||
|
// No read receipt for this item
|
||||||
|
holder.readReceiptsView.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
protected val distinctMergeData by lazy {
|
||||||
|
attributes.mergeData.distinctBy { it.userId }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getEventIds(): List<String> {
|
||||||
|
return if (attributes.isCollapsed) {
|
||||||
|
attributes.mergeData.map { it.eventId }
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val localId: Long,
|
||||||
|
val eventId: String,
|
||||||
|
val userId: String,
|
||||||
|
val memberName: String,
|
||||||
|
val avatarUrl: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
|
||||||
|
|
||||||
|
interface Attributes {
|
||||||
|
val isCollapsed: Boolean
|
||||||
|
val mergeData: List<Data>
|
||||||
|
val avatarRenderer: AvatarRenderer
|
||||||
|
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
|
||||||
|
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||||
|
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
|
||||||
|
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,28 +24,20 @@ import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import com.airbnb.epoxy.EpoxyAttribute
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
import com.airbnb.epoxy.EpoxyModelClass
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
import im.vector.matrix.android.api.util.MatrixItem
|
|
||||||
import im.vector.riotx.R
|
import im.vector.riotx.R
|
||||||
import im.vector.riotx.features.home.AvatarRenderer
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
|
||||||
|
|
||||||
@EpoxyAttribute
|
|
||||||
lateinit var attributes: Attributes
|
|
||||||
|
|
||||||
private val distinctMergeData by lazy {
|
|
||||||
attributes.mergeData.distinctBy { it.userId }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewType() = STUB_ID
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
override lateinit var attributes: Attributes
|
||||||
|
|
||||||
override fun bind(holder: Holder) {
|
override fun bind(holder: Holder) {
|
||||||
super.bind(holder)
|
super.bind(holder)
|
||||||
holder.expandView.setOnClickListener {
|
|
||||||
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
|
|
||||||
}
|
|
||||||
if (attributes.isCollapsed) {
|
if (attributes.isCollapsed) {
|
||||||
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
|
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
|
||||||
holder.summaryView.text = summary
|
holder.summaryView.text = summary
|
||||||
|
@ -60,52 +52,28 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
|
||||||
view.visibility = View.GONE
|
view.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.separatorView.visibility = View.GONE
|
|
||||||
holder.expandView.setText(R.string.merged_events_expand)
|
|
||||||
} else {
|
} else {
|
||||||
holder.avatarListView.visibility = View.INVISIBLE
|
holder.avatarListView.visibility = View.INVISIBLE
|
||||||
holder.summaryView.visibility = View.GONE
|
holder.summaryView.visibility = View.GONE
|
||||||
holder.separatorView.visibility = View.VISIBLE
|
|
||||||
holder.expandView.setText(R.string.merged_events_collapse)
|
|
||||||
}
|
}
|
||||||
// No read receipt for this item
|
// No read receipt for this item
|
||||||
holder.readReceiptsView.isVisible = false
|
holder.readReceiptsView.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getEventIds(): List<String> {
|
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||||
return if (attributes.isCollapsed) {
|
|
||||||
attributes.mergeData.map { it.eventId }
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class Data(
|
|
||||||
val localId: Long,
|
|
||||||
val eventId: String,
|
|
||||||
val userId: String,
|
|
||||||
val memberName: String,
|
|
||||||
val avatarUrl: String?
|
|
||||||
)
|
|
||||||
|
|
||||||
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
|
|
||||||
|
|
||||||
data class Attributes(
|
|
||||||
val isCollapsed: Boolean,
|
|
||||||
val mergeData: List<Data>,
|
|
||||||
val avatarRenderer: AvatarRenderer,
|
|
||||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
|
||||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
|
||||||
)
|
|
||||||
|
|
||||||
class Holder : BaseHolder(STUB_ID) {
|
|
||||||
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
|
|
||||||
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
|
||||||
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
|
||||||
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
|
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val STUB_ID = R.id.messageContentMergedHeaderStub
|
private const val STUB_ID = R.id.messageContentMergedHeaderStub
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data class Attributes(
|
||||||
|
override val isCollapsed: Boolean,
|
||||||
|
override val mergeData: List<Data>,
|
||||||
|
override val avatarRenderer: AvatarRenderer,
|
||||||
|
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
|
override val onCollapsedStateChanged: (Boolean) -> Unit
|
||||||
|
) : BasedMergedItem.Attributes
|
||||||
}
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2020 New Vector Ltd
|
||||||
|
*
|
||||||
|
* 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 im.vector.riotx.features.home.room.detail.timeline.item
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.RelativeLayout
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
|
import com.airbnb.epoxy.EpoxyAttribute
|
||||||
|
import com.airbnb.epoxy.EpoxyModelClass
|
||||||
|
import im.vector.riotx.R
|
||||||
|
import im.vector.riotx.features.home.AvatarRenderer
|
||||||
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||||
|
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
|
||||||
|
|
||||||
|
@EpoxyAttribute
|
||||||
|
override lateinit var attributes: Attributes
|
||||||
|
|
||||||
|
override fun getViewType() = STUB_ID
|
||||||
|
|
||||||
|
override fun bind(holder: Holder) {
|
||||||
|
super.bind(holder)
|
||||||
|
|
||||||
|
if (attributes.isCollapsed) {
|
||||||
|
val data = distinctMergeData.firstOrNull()
|
||||||
|
|
||||||
|
val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
|
||||||
|
data?.memberName ?: data?.userId ?: "")
|
||||||
|
holder.summaryView.text = summary
|
||||||
|
holder.summaryView.visibility = View.VISIBLE
|
||||||
|
holder.avatarView.visibility = View.VISIBLE
|
||||||
|
if (data != null) {
|
||||||
|
holder.avatarView.visibility = View.VISIBLE
|
||||||
|
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||||
|
} else {
|
||||||
|
holder.avatarView.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attributes.hasEncryptionEvent) {
|
||||||
|
holder.encryptionTile.isVisible = true
|
||||||
|
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||||
|
this.marginEnd = leftGuideline
|
||||||
|
}
|
||||||
|
if (attributes.isEncryptionAlgorithmSecure) {
|
||||||
|
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||||
|
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||||
|
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
|
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||||
|
null, null, null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
||||||
|
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||||
|
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||||
|
null, null, null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.encryptionTile.isVisible = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
holder.avatarView.visibility = View.INVISIBLE
|
||||||
|
holder.summaryView.visibility = View.GONE
|
||||||
|
holder.encryptionTile.isGone = true
|
||||||
|
}
|
||||||
|
// No read receipt for this item
|
||||||
|
holder.readReceiptsView.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||||
|
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||||
|
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||||
|
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
|
||||||
|
|
||||||
|
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
|
||||||
|
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val STUB_ID = R.id.messageContentMergedCreationStub
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Attributes(
|
||||||
|
override val isCollapsed: Boolean,
|
||||||
|
override val mergeData: List<Data>,
|
||||||
|
override val avatarRenderer: AvatarRenderer,
|
||||||
|
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
|
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||||
|
val hasEncryptionEvent : Boolean,
|
||||||
|
val isEncryptionAlgorithmSecure: Boolean
|
||||||
|
) : BasedMergedItem.Attributes
|
||||||
|
}
|
|
@ -31,7 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
||||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||||
|
|
||||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
|
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
|
||||||
abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<VerificationRequestConclusionItem.Holder>() {
|
abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineItem.Holder>() {
|
||||||
|
|
||||||
override val baseAttributes: AbsBaseMessageItem.Attributes
|
override val baseAttributes: AbsBaseMessageItem.Attributes
|
||||||
get() = attributes
|
get() = attributes
|
||||||
|
@ -47,11 +47,17 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
|
||||||
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
|
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||||
this.marginEnd = leftGuideline
|
this.marginEnd = leftGuideline
|
||||||
}
|
}
|
||||||
val title = if (attributes.isPositive) R.string.sas_verified else R.string.verification_conclusion_warning
|
|
||||||
holder.titleView.text = holder.view.context.getString(title)
|
|
||||||
holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
|
|
||||||
|
|
||||||
val startDrawable = if (attributes.isPositive) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
|
holder.titleView.text = attributes.title
|
||||||
|
holder.descriptionView.text = attributes.description
|
||||||
|
holder.descriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||||
|
|
||||||
|
val startDrawable = when (attributes.shieldUIState) {
|
||||||
|
ShieldUIState.GREEN -> R.drawable.ic_shield_trusted
|
||||||
|
ShieldUIState.BLACK -> R.drawable.ic_shield_black
|
||||||
|
ShieldUIState.RED -> R.drawable.ic_shield_warning
|
||||||
|
}
|
||||||
|
|
||||||
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
|
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
ContextCompat.getDrawable(holder.view.context, startDrawable),
|
ContextCompat.getDrawable(holder.view.context, startDrawable),
|
||||||
null, null, null
|
null, null, null
|
||||||
|
@ -75,9 +81,9 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
|
||||||
* This class holds all the common attributes for timeline items.
|
* This class holds all the common attributes for timeline items.
|
||||||
*/
|
*/
|
||||||
data class Attributes(
|
data class Attributes(
|
||||||
val toUserId: String,
|
val shieldUIState: ShieldUIState,
|
||||||
val toUserName: String,
|
val title: CharSequence,
|
||||||
val isPositive: Boolean,
|
val description: CharSequence,
|
||||||
override val informationData: MessageInformationData,
|
override val informationData: MessageInformationData,
|
||||||
override val avatarRenderer: AvatarRenderer,
|
override val avatarRenderer: AvatarRenderer,
|
||||||
override val messageColorProvider: MessageColorProvider,
|
override val messageColorProvider: MessageColorProvider,
|
||||||
|
@ -87,4 +93,10 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
|
||||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||||
val emojiTypeFace: Typeface? = null
|
val emojiTypeFace: Typeface? = null
|
||||||
) : AbsBaseMessageItem.Attributes
|
) : AbsBaseMessageItem.Attributes
|
||||||
|
|
||||||
|
enum class ShieldUIState {
|
||||||
|
BLACK,
|
||||||
|
RED,
|
||||||
|
GREEN
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -54,6 +54,13 @@
|
||||||
tools:layout_marginTop="160dp"
|
tools:layout_marginTop="160dp"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/messageContentMergedCreationStub"
|
||||||
|
style="@style/TimelineContentStubBaseParams"
|
||||||
|
android:layout="@layout/item_timeline_event_merged_room_creation_stub"
|
||||||
|
tools:layout_marginTop="160dp"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
<im.vector.riotx.core.ui.views.ReadReceiptsView
|
||||||
|
|
|
@ -49,7 +49,7 @@
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/messageVerificationDoneStub"
|
android:id="@+id/messageVerificationDoneStub"
|
||||||
style="@style/TimelineContentStubBaseParams"
|
style="@style/TimelineContentStubBaseParams"
|
||||||
android:layout="@layout/item_timeline_event_verification_done_stub"
|
android:layout="@layout/item_timeline_event_status_tile_stub"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/creationEncryptionTile"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:layout_marginEnd="52dp"
|
||||||
|
android:layout_marginBottom="2dp"
|
||||||
|
android:background="@drawable/rounded_rect_shape_8"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<include layout="@layout/item_timeline_event_status_tile_stub" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/mergedSumContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/creationEncryptionTile">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/itemNoticeAvatarView"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
tools:srcCompat="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemNoticeTextView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_toStartOf="@id/itemMergedExpandTextView"
|
||||||
|
android:layout_toEndOf="@id/itemNoticeAvatarView"
|
||||||
|
android:textColor="?riotx_text_secondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="italic"
|
||||||
|
tools:text="@string/room_created_summary_item" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/itemMergedExpandTextView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:text="@string/merged_events_expand"
|
||||||
|
android:textColor="?attr/colorAccent"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textStyle="italic" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/itemMergedSeparatorView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_below="@+id/mergedSumContainer"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/riotx_header_panel_background" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
|
@ -87,6 +87,13 @@
|
||||||
<string name="bootstrap_skip_text">Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
<string name="bootstrap_skip_text">Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
||||||
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure & unlock encrypted messages and trust.</string>
|
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure & unlock encrypted messages and trust.</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="encryption_enabled">Encryption enabled</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="encryption_not_enabled">Encryption not enabled</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>
|
||||||
<!-- END Strings added by Valere -->
|
<!-- END Strings added by Valere -->
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue