From 12abca1b803e4efd8e6602881b49a5a15f057009 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2020 19:09:56 +0200 Subject: [PATCH 1/9] Fix / Send gossip request on other done received --- .../crypto/gossiping/KeyShareTests.kt | 2 +- .../DefaultVerificationService.kt | 56 ++++++++++++++++++- .../DefaultVerificationTransaction.kt | 19 +++---- .../verification/VerificationInfoDone.kt | 8 +-- 4 files changed, 67 insertions(+), 18 deletions(-) diff --git a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt index 7ac92ed74c..bb6e020d89 100644 --- a/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt +++ b/matrix-sdk-android/src/androidTest/java/im/vector/matrix/android/internal/crypto/gossiping/KeyShareTests.kt @@ -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 } } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt index dc68fa6b76..db6c224019 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationService.kt @@ -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>() + // 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>() + /** * 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()?.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() ?.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) diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt index 5b8191fc99..29cfcd2383 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/DefaultVerificationTransaction.kt @@ -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) { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt index abb1141355..2986013fca 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/crypto/verification/VerificationInfoDone.kt @@ -18,11 +18,9 @@ package im.vector.matrix.android.internal.crypto.verification internal interface VerificationInfoDone : VerificationInfo { 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) From b480eb36884c01cee41b044d16211bb3cb2d0da4 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2020 19:20:49 +0200 Subject: [PATCH 2/9] Update change log --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 74269178dd..4dccae5ad5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -19,6 +19,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 πŸ—£: - From 68512e475ff6c71f90a7abede9a49210eb5a7ffe Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 7 Apr 2020 13:49:39 +0200 Subject: [PATCH 3/9] WIP --- .../timeline/TimelineEventController.kt | 4 +- .../factory/MergedHeaderItemFactory.kt | 78 +++++++++++++++++-- .../helper/TimelineDisplayableEvents.kt | 12 +++ .../detail/timeline/item/BasedMergedItem.kt | 63 +++++++++++++++ .../detail/timeline/item/MergedHeaderItem.kt | 41 +--------- 5 files changed, 152 insertions(+), 46 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index a8c9cf679b..48e92ca438 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 42dc4e07eb..a193efe076 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -16,6 +16,7 @@ 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.room.timeline.TimelineEvent import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.features.home.AvatarRenderer @@ -23,7 +24,10 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle 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.BaseEventItem +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.MergedHeaderItem_ import javax.inject.Inject @@ -43,8 +47,72 @@ 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 + var prevEvent = if (currentPosition > 0) items[currentPosition -1] else null + var tmpPos = currentPosition -1 + val mergedEvents = ArrayList().also { it.add(event) } + while(prevEvent != null && prevEvent.isRoomConfiguration()) { + mergedEvents.add(prevEvent) + tmpPos-- + prevEvent = if (tmpPos >= 0) items[tmpPos] else null + } + if (mergedEvents.size > 2) { + var highlighted = false + val mergedData = ArrayList(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 = BasedMergedItem.Attributes( + isCollapsed = isCollapsed, + mergeData = mergedData, + avatarRenderer = avatarRenderer, + onCollapsedStateChanged = { + mergeItemCollapseStates[event.localId] = it + requestModelBuild() + }, + readReceiptsCallback = callback + ) + MergedHeaderItem_() + .id(mergeId) + .leftGuideline(avatarSizeProvider.leftGuideline) + .highlighted(isCollapsed && highlighted) + .attributes(attributes) + .also { + it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) + } + + } else null + } + else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { null } else { val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) @@ -53,14 +121,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } else { var highlighted = false val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() - val mergedData = ArrayList(mergedEvents.size) + val mergedData = ArrayList(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 +150,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 = BasedMergedItem.Attributes( isCollapsed = isCollapsed, mergeData = mergedData, avatarRenderer = avatarRenderer, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index 5c763cb114..1ea3cd64ac 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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.nextSameTypeEvents(index: Int, minSize: Int): List { if (index >= size - 1) { return emptyList() diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt new file mode 100644 index 0000000000..832b4017a7 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -0,0 +1,63 @@ +/* + * 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 androidx.annotation.IdRes +import com.airbnb.epoxy.EpoxyAttribute +import im.vector.matrix.android.api.util.MatrixItem +import im.vector.riotx.features.home.AvatarRenderer +import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController + +abstract class BasedMergedItem : BaseEventItem() { + + @EpoxyAttribute + lateinit var attributes: Attributes + + protected val distinctMergeData by lazy { + attributes.mergeData.distinctBy { it.userId } + } + + override fun getEventIds(): List { + 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, + val avatarRenderer: AvatarRenderer, + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + val onCollapsedStateChanged: (Boolean) -> Unit + ) + + abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { + //val reactionsContainer by bind(R.id.reactionsContainer) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 93f7dc271d..2d6bb0f85d 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -22,22 +22,11 @@ import android.widget.ImageView import android.widget.TextView 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() { - - @EpoxyAttribute - lateinit var attributes: Attributes - - private val distinctMergeData by lazy { - attributes.mergeData.distinctBy { it.userId } - } +abstract class MergedHeaderItem : BasedMergedItem() { override fun getViewType() = STUB_ID @@ -72,33 +61,7 @@ abstract class MergedHeaderItem : BaseEventItem() { holder.readReceiptsView.isVisible = false } - override fun getEventIds(): List { - 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, - val avatarRenderer: AvatarRenderer, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, - val onCollapsedStateChanged: (Boolean) -> Unit - ) - - class Holder : BaseHolder(STUB_ID) { + class Holder : BasedMergedItem.Holder(STUB_ID) { val expandView by bind(R.id.itemMergedExpandTextView) val summaryView by bind(R.id.itemMergedSummaryTextView) val separatorView by bind(R.id.itemMergedSeparatorView) From 277f35a352f57047881b2fe1d2e061ad0a549a23 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 14:39:57 +0200 Subject: [PATCH 4/9] Merge room creation events in one summary --- .../timeline/factory/EncryptionItemFactory.kt | 81 ++++++++++ .../factory/MergedHeaderItemFactory.kt | 142 ++++++++++-------- .../timeline/factory/TimelineItemFactory.kt | 7 +- .../factory/VerificationItemFactory.kt | 27 ++-- .../detail/timeline/item/BasedMergedItem.kt | 41 +++-- .../detail/timeline/item/MergedHeaderItem.kt | 23 +-- .../timeline/item/MergedRoomCreationItem.kt | 118 +++++++++++++++ ...usionItem.kt => StatusTileTimelineItem.kt} | 27 +++- .../item_timeline_event_base_noinfo.xml | 7 + ...meline_event_merged_room_creation_stub.xml | 80 ++++++++++ vector/src/main/res/values/strings_riotX.xml | 7 + 11 files changed, 454 insertions(+), 106 deletions(-) create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt create mode 100644 vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/{VerificationRequestConclusionItem.kt => StatusTileTimelineItem.kt} (82%) create mode 100644 vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt new file mode 100644 index 0000000000..a175fc6e84 --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -0,0 +1,81 @@ +/* + * 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()?.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) + } +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index a193efe076..3d95d1d49b 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -17,7 +17,10 @@ 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 @@ -26,10 +29,11 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineE 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.BaseEventItem 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.MergedHeaderItem_ +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, @@ -51,68 +55,8 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration()) { // It's the first item before room.create // Collapse all room configuration events - var prevEvent = if (currentPosition > 0) items[currentPosition -1] else null - var tmpPos = currentPosition -1 - val mergedEvents = ArrayList().also { it.add(event) } - while(prevEvent != null && prevEvent.isRoomConfiguration()) { - mergedEvents.add(prevEvent) - tmpPos-- - prevEvent = if (tmpPos >= 0) items[tmpPos] else null - } - if (mergedEvents.size > 2) { - var highlighted = false - val mergedData = ArrayList(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 = BasedMergedItem.Attributes( - isCollapsed = isCollapsed, - mergeData = mergedData, - avatarRenderer = avatarRenderer, - onCollapsedStateChanged = { - mergeItemCollapseStates[event.localId] = it - requestModelBuild() - }, - readReceiptsCallback = callback - ) - MergedHeaderItem_() - .id(mergeId) - .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(isCollapsed && highlighted) - .attributes(attributes) - .also { - it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents)) - } - - } else null - } - else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) { + 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) @@ -150,7 +94,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act collapsedEventIds.removeAll(mergedEventIds) } val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() } - val attributes = BasedMergedItem.Attributes( + val attributes = MergedHeaderItem.Attributes( isCollapsed = isCollapsed, mergeData = mergedData, avatarRenderer = avatarRenderer, @@ -172,6 +116,76 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } } + private fun buildRoomCreationMergedSummary(currentPosition: Int, items: List, 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().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()?.algorithm + } + mergedEvents.add(prevEvent) + tmpPos-- + prevEvent = if (tmpPos >= 0) items[tmpPos] else null + } + return if (mergedEvents.size > 2) { + var highlighted = false + val mergedData = ArrayList(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) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 1462f5fe0d..7e6c387934 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index ee529282f9..3aa43120ea 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -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.toString()} (${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.toString()} (${informationData.senderId})", + shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN, informationData = informationData, avatarRenderer = attributes.avatarRenderer, messageColorProvider = messageColorProvider, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt index 832b4017a7..adc9b1442f 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -16,16 +16,34 @@ package im.vector.riotx.features.home.room.detail.timeline.item +import android.view.View +import android.widget.TextView import androidx.annotation.IdRes -import com.airbnb.epoxy.EpoxyAttribute +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 : BaseEventItem() { - @EpoxyAttribute - lateinit var attributes: Attributes + 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 } @@ -49,15 +67,16 @@ abstract class BasedMergedItem : BaseEventItem() fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl) - data class Attributes( - val isCollapsed: Boolean, - val mergeData: List, - val avatarRenderer: AvatarRenderer, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, - val onCollapsedStateChanged: (Boolean) -> Unit - ) + interface Attributes { + val isCollapsed: Boolean + val mergeData: List + val avatarRenderer: AvatarRenderer + val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? + val onCollapsedStateChanged: (Boolean) -> Unit + } abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) { - //val reactionsContainer by bind(R.id.reactionsContainer) + val expandView by bind(R.id.itemMergedExpandTextView) + val separatorView by bind(R.id.itemMergedSeparatorView) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt index 2d6bb0f85d..59f4d08d68 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt @@ -22,19 +22,22 @@ import android.widget.ImageView import android.widget.TextView import androidx.core.view.children import androidx.core.view.isVisible +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 MergedHeaderItem : BasedMergedItem() { 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 @@ -49,26 +52,28 @@ abstract class MergedHeaderItem : BasedMergedItem() { 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 } class Holder : BasedMergedItem.Holder(STUB_ID) { - val expandView by bind(R.id.itemMergedExpandTextView) val summaryView by bind(R.id.itemMergedSummaryTextView) - val separatorView by bind(R.id.itemMergedSeparatorView) val avatarListView by bind(R.id.itemMergedAvatarListView) } companion object { private const val STUB_ID = R.id.messageContentMergedHeaderStub } + + data class Attributes( + override val isCollapsed: Boolean, + override val mergeData: List, + override val avatarRenderer: AvatarRenderer, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + override val onCollapsedStateChanged: (Boolean) -> Unit + ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt new file mode 100644 index 0000000000..78e6ffe67b --- /dev/null +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -0,0 +1,118 @@ +/* + * 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.marginLeft +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 +import org.w3c.dom.Text + +@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) +abstract class MergedRoomCreationItem : BasedMergedItem() { + + @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 { + 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(R.id.itemNoticeTextView) + val avatarView by bind(R.id.itemNoticeAvatarView) + val encryptionTile by bind(R.id.creationEncryptionTile) + + val e2eTitleTextView by bind(R.id.itemVerificationDoneTitleTextView) + val e2eTitleDescriptionView by bind(R.id.itemVerificationDoneDetailTextView) + } + + companion object { + private const val STUB_ID = R.id.messageContentMergedCreationStub + } + + data class Attributes( + override val isCollapsed: Boolean, + override val mergeData: List, + override val avatarRenderer: AvatarRenderer, + override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, + override val onCollapsedStateChanged: (Boolean) -> Unit, + val hasEncryptionEvent : Boolean, + val isEncryptionAlgorithmSecure: Boolean + ) : BasedMergedItem.Attributes +} diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt similarity index 82% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 2b28e15cab..79ffb3a6eb 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/VerificationRequestConclusionItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -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() { +abstract class StatusTileTimelineItem : AbsBaseMessageItem() { override val baseAttributes: AbsBaseMessageItem.Attributes get() = attributes @@ -47,11 +47,16 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem { 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 + + 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 +80,9 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 29e57d1133..606a1b95f6 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -87,6 +87,13 @@ 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. Setting a Message Password lets you secure & unlock encrypted messages and trust. + + Encryption enabled + Messages in this room are end-to-end encrypted. Learn more & verify users in their profile. + Encryption not enabled + The encryption used by this room is not supported + + %s created and configured the room. From 08af61b77819cbe5725ca6731857cec2aa6d54d1 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 14:41:57 +0200 Subject: [PATCH 5/9] rename layout --- vector/src/main/res/layout/item_timeline_event_base_state.xml | 2 +- .../layout/item_timeline_event_merged_room_creation_stub.xml | 2 +- ...n_done_stub.xml => item_timeline_event_status_tile_stub.xml} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename vector/src/main/res/layout/{item_timeline_event_verification_done_stub.xml => item_timeline_event_status_tile_stub.xml} (100%) diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index 5ad58b5201..af20c3f549 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -49,7 +49,7 @@ diff --git a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml index 56f38f2276..3ed840954f 100644 --- a/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml +++ b/vector/src/main/res/layout/item_timeline_event_merged_room_creation_stub.xml @@ -17,7 +17,7 @@ android:background="@drawable/rounded_rect_shape_8" android:padding="8dp"> - + diff --git a/vector/src/main/res/layout/item_timeline_event_verification_done_stub.xml b/vector/src/main/res/layout/item_timeline_event_status_tile_stub.xml similarity index 100% rename from vector/src/main/res/layout/item_timeline_event_verification_done_stub.xml rename to vector/src/main/res/layout/item_timeline_event_status_tile_stub.xml From f04d8b0e03c0099592d91da3b127c9117bb92346 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 14:43:32 +0200 Subject: [PATCH 6/9] cleaning --- .../room/detail/timeline/factory/EncryptionItemFactory.kt | 1 - .../detail/timeline/factory/MergedHeaderItemFactory.kt | 7 ++++++- .../detail/timeline/factory/VerificationItemFactory.kt | 4 ++-- .../room/detail/timeline/item/MergedRoomCreationItem.kt | 3 --- .../room/detail/timeline/item/StatusTileTimelineItem.kt | 2 +- 5 files changed, 9 insertions(+), 8 deletions(-) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index a175fc6e84..ff65b0e656 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -41,7 +41,6 @@ class EncryptionItemFactory @Inject constructor( fun create(event: TimelineEvent, highlight: Boolean, callback: TimelineEventController.Callback?): StatusTileTimelineItem? { - val algorithm = event.root.getClearContent().toModel()?.algorithm val informationData = informationDataFactory.create(event, null) val attributes = messageItemAttributesFactory.create(null, informationData, callback) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 3d95d1d49b..00e90558a6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -116,7 +116,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act } } - private fun buildRoomCreationMergedSummary(currentPosition: Int, items: List, event: TimelineEvent, eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedRoomCreationItem_? { + private fun buildRoomCreationMergedSummary(currentPosition: Int, + items: List, + 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().also { it.add(event) } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 3aa43120ea..837d0ad571 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -95,7 +95,7 @@ class VerificationItemFactory @Inject constructor( .attributes( StatusTileTimelineItem.Attributes( title = stringProvider.getString(R.string.verification_conclusion_warning), - description = "${informationData.memberName.toString()} (${informationData.senderId})", + description = "${informationData.memberName} (${informationData.senderId})", shieldUIState = StatusTileTimelineItem.ShieldUIState.RED, informationData = informationData, avatarRenderer = attributes.avatarRenderer, @@ -128,7 +128,7 @@ class VerificationItemFactory @Inject constructor( .attributes( StatusTileTimelineItem.Attributes( title = stringProvider.getString(R.string.sas_verified), - description = "${informationData.memberName.toString()} (${informationData.senderId})", + description = "${informationData.memberName} (${informationData.senderId})", shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN, informationData = informationData, avatarRenderer = attributes.avatarRenderer, diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 78e6ffe67b..81050194a8 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -24,14 +24,12 @@ import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible -import androidx.core.view.marginLeft 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 -import org.w3c.dom.Text @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedRoomCreationItem : BasedMergedItem() { @@ -45,7 +43,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem R.drawable.ic_shield_black ShieldUIState.RED -> R.drawable.ic_shield_warning } - + holder.titleView.setCompoundDrawablesWithIntrinsicBounds( ContextCompat.getDrawable(holder.view.context, startDrawable), null, null, null From aec49fe542fc6a98e71fa1f1662f10af58707819 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 14:56:37 +0200 Subject: [PATCH 7/9] Change log --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 4dccae5ad5..983c9841cd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) Bugfix πŸ›: - Missing avatar/displayname after verification request message (#841) From 366a35913b4833ecb9e0c8dc6efe7e2ab585dcf6 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 15:05:49 +0200 Subject: [PATCH 8/9] Fix alignement --- .../home/room/detail/timeline/item/StatusTileTimelineItem.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt index 43c53c7267..f9ea2a71df 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/StatusTileTimelineItem.kt @@ -50,6 +50,7 @@ abstract class StatusTileTimelineItem : AbsBaseMessageItem R.drawable.ic_shield_trusted From 6f2d7aebbab91b469493874ab6b23729a3bd675b Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 8 Apr 2020 16:37:52 +0200 Subject: [PATCH 9/9] code review --- .../detail/timeline/factory/MergedHeaderItemFactory.kt | 8 ++++---- ...{MergedHeaderItem.kt => MergedMembershipEventsItem.kt} | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) rename vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/{MergedHeaderItem.kt => MergedMembershipEventsItem.kt} (96%) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 00e90558a6..377fc5ab4a 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -30,8 +30,8 @@ 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.BasedMergedItem -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.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 @@ -94,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, @@ -104,7 +104,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act }, readReceiptsCallback = callback ) - MergedHeaderItem_() + MergedMembershipEventsItem_() .id(mergeId) .leftGuideline(avatarSizeProvider.leftGuideline) .highlighted(isCollapsed && highlighted) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt similarity index 96% rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt index 59f4d08d68..8e3ba0bcff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedHeaderItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt @@ -29,7 +29,7 @@ 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 : BasedMergedItem() { +abstract class MergedMembershipEventsItem : BasedMergedItem() { override fun getViewType() = STUB_ID