Merge branch 'develop' into improvement-957-catchup-indicator-on-invite

This commit is contained in:
Christopher Rossbach 2020-04-09 11:11:44 +02:00 committed by GitHub
commit ef2abbfbd4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 604 additions and 96 deletions

View file

@ -6,12 +6,14 @@ Features ✨:
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)
- Cross-Signing | Update Shield Logic for DM (#963)
- Cross-Signing | Complete security new session design update (#1135)
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
- 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)
Bugfix 🐛:
@ -20,6 +22,7 @@ Bugfix 🐛:
- 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)
- 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 🗣:
-

View file

@ -282,7 +282,7 @@ class KeyShareTests : InstrumentedTest {
val keysBackupService = aliceSession2.cryptoService().keysBackupService()
mTestHelper.retryPeriodicallyWithLatch(latch) {
Log.d("#TEST", "Recovery :${ keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey}")
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey != creationInfo.recoveryKey
keysBackupService.getKeyBackupRecoveryKeyInfo()?.recoveryKey == creationInfo.recoveryKey
}
}
}

View file

@ -22,6 +22,9 @@ import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
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.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.PendingVerificationRequest
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.rest.KeyVerificationAccept
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.KeyVerificationMac
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationReady
@ -109,6 +113,10 @@ internal class DefaultVerificationService @Inject constructor(
// map [sender : [transaction]]
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]]
* 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 -> {
onReadyReceived(event)
}
EventType.KEY_VERIFICATION_DONE -> {
onDoneReceived(event)
}
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
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) {
val doneReq = event.getClearContent().toModel<MessageVerificationDoneContent>()
?.copy(
@ -1003,7 +1039,11 @@ internal class DefaultVerificationService @Inject constructor(
private fun removeTransaction(otherUser: String, tid: String) {
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? {
val txID = transactionId?.takeIf { it.isNotEmpty() } ?: createUniqueIDForTransaction(otherUserId, otherDeviceId)
// should check if already one (and cancel it)

View file

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

View file

@ -18,11 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification
internal interface VerificationInfoDone : VerificationInfo<ValidVerificationInfoDone> {
override fun asValidObject(): ValidVerificationInfoDone? {
if (transactionId.isNullOrEmpty()) {
return null
}
return ValidVerificationInfoDone
val validTransactionId = transactionId?.takeIf { it.isNotEmpty() } ?: return null
return ValidVerificationInfoDone(validTransactionId)
}
}
internal object ValidVerificationInfoDone
internal data class ValidVerificationInfoDone(val transactionId: String)

View file

@ -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.nextOrNull
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.MergedHeaderItem
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.TimelineReadMarkerItem_
@ -373,7 +373,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val localId: Long,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null
) {
fun shouldTriggerBuild(): Boolean {

View file

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

View file

@ -16,16 +16,24 @@
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.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.features.home.AvatarRenderer
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.MergedTimelineEventVisibilityStateChangedListener
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.item.MergedHeaderItem
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.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
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
@ -43,8 +51,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit)
: MergedHeaderItem? {
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
: BasedMergedItem<*>? {
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
} else {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
@ -53,14 +65,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
} else {
var highlighted = false
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = MergedHeaderItem.Data(
val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName,
@ -82,7 +94,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedHeaderItem.Attributes(
val attributes = MergedMembershipEventsItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
@ -92,7 +104,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
},
readReceiptsCallback = callback
)
MergedHeaderItem_()
MergedMembershipEventsItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.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 {
return collapsedEventIds.contains(localId)
}

View file

@ -29,6 +29,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory,
private val encryptionItemFactory: EncryptionItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) {
@ -57,8 +58,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.CALL_HANGUP,
EventType.CALL_ANSWER,
EventType.REACTION,
EventType.REDACTION,
EventType.STATE_ROOM_ENCRYPTION -> noticeItemFactory.create(event, highlight, callback)
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> {
encryptionItemFactory.create(event, highlight, callback)
}
// State room create
EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback)
// Crypto

View file

@ -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.timeline.TimelineEvent
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.resources.StringProvider
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.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.VerificationRequestConclusionItem
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.StatusTileTimelineItem_
import javax.inject.Inject
/**
@ -48,6 +50,7 @@ class VerificationItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory,
private val userPreferencesProvider: UserPreferencesProvider,
private val stringProvider: StringProvider,
private val session: Session
) {
@ -88,12 +91,12 @@ class VerificationItemFactory @Inject constructor(
CancelCode.MismatchedKeys,
CancelCode.MismatchedSas -> {
// We should display these bad conclusions
return VerificationRequestConclusionItem_()
return StatusTileTimelineItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = false,
StatusTileTimelineItem.Attributes(
title = stringProvider.getString(R.string.verification_conclusion_warning),
description = "${informationData.memberName} (${informationData.senderId})",
shieldUIState = StatusTileTimelineItem.ShieldUIState.RED,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
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
return ignoredConclusion(event, highlight, callback)
}
return VerificationRequestConclusionItem_()
return StatusTileTimelineItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = true,
StatusTileTimelineItem.Attributes(
title = stringProvider.getString(R.string.sas_verified),
description = "${informationData.memberName} (${informationData.senderId})",
shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = messageColorProvider,

View file

@ -50,6 +50,18 @@ fun TimelineEvent.canBeMerged(): Boolean {
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> {
if (index >= size - 1) {
return emptyList()

View file

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

View file

@ -24,28 +24,20 @@ import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
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
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val distinctMergeData by lazy {
attributes.mergeData.distinctBy { it.userId }
}
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
override fun getViewType() = STUB_ID
@EpoxyAttribute
override lateinit var attributes: Attributes
override fun bind(holder: Holder) {
super.bind(holder)
holder.expandView.setOnClickListener {
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
}
if (attributes.isCollapsed) {
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
holder.summaryView.text = summary
@ -60,52 +52,28 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
view.visibility = View.GONE
}
}
holder.separatorView.visibility = View.GONE
holder.expandView.setText(R.string.merged_events_expand)
} else {
holder.avatarListView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse)
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
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)
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)
class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
}
companion object {
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
}

View file

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

View file

@ -31,7 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@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
get() = attributes
@ -47,11 +47,17 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
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(
ContextCompat.getDrawable(holder.view.context, startDrawable),
null, null, null
@ -75,9 +81,9 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
* This class holds all the common attributes for timeline items.
*/
data class Attributes(
val toUserId: String,
val toUserName: String,
val isPositive: Boolean,
val shieldUIState: ShieldUIState,
val title: CharSequence,
val description: CharSequence,
override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer,
override val messageColorProvider: MessageColorProvider,
@ -87,4 +93,10 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
) : AbsBaseMessageItem.Attributes
enum class ShieldUIState {
BLACK,
RED,
GREEN
}
}

View file

@ -54,6 +54,13 @@
tools:layout_marginTop="160dp"
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>
<im.vector.riotx.core.ui.views.ReadReceiptsView

View file

@ -49,7 +49,7 @@
<ViewStub
android:id="@+id/messageVerificationDoneStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_verification_done_stub"
android:layout="@layout/item_timeline_event_status_tile_stub"
tools:visibility="visible" />
</FrameLayout>

View file

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

View file

@ -87,6 +87,13 @@
<string name="bootstrap_skip_text">Setting a Message Password lets you secure &amp; unlock encrypted messages and trust.\n\nIf you dont 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 &amp; 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 &amp; 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 -->