mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2024-11-26 03:16:02 +03:00
Merge branch 'develop' into improvement-957-catchup-indicator-on-invite
This commit is contained in:
commit
ef2abbfbd4
20 changed files with 604 additions and 96 deletions
|
@ -6,12 +6,14 @@ Features ✨:
|
|||
- Cross-Signing | Verify new session from existing session (#1134)
|
||||
- Cross-Signing | 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 🗣:
|
||||
-
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||
|
||||
import im.vector.matrix.android.api.session.events.model.toModel
|
||||
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
|
||||
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem
|
||||
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_
|
||||
import javax.inject.Inject
|
||||
|
||||
class EncryptionItemFactory @Inject constructor(
|
||||
private val messageItemAttributesFactory: MessageItemAttributesFactory,
|
||||
private val messageColorProvider: MessageColorProvider,
|
||||
private val stringProvider: StringProvider,
|
||||
private val informationDataFactory: MessageInformationDataFactory,
|
||||
private val avatarSizeProvider: AvatarSizeProvider) {
|
||||
|
||||
fun create(event: TimelineEvent,
|
||||
highlight: Boolean,
|
||||
callback: TimelineEventController.Callback?): StatusTileTimelineItem? {
|
||||
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
|
||||
val informationData = informationDataFactory.create(event, null)
|
||||
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
|
||||
|
||||
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
|
||||
val title: String
|
||||
val description: String
|
||||
val shield: StatusTileTimelineItem.ShieldUIState
|
||||
if (isSafeAlgorithm) {
|
||||
title = stringProvider.getString(R.string.encryption_enabled)
|
||||
description = stringProvider.getString(R.string.encryption_enabled_tile_description)
|
||||
shield = StatusTileTimelineItem.ShieldUIState.BLACK
|
||||
} else {
|
||||
title = stringProvider.getString(R.string.encryption_not_enabled)
|
||||
description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||
shield = StatusTileTimelineItem.ShieldUIState.RED
|
||||
}
|
||||
return StatusTileTimelineItem_()
|
||||
.attributes(
|
||||
StatusTileTimelineItem.Attributes(
|
||||
title = title,
|
||||
description = description,
|
||||
shieldUIState = shield,
|
||||
informationData = informationData,
|
||||
avatarRenderer = attributes.avatarRenderer,
|
||||
messageColorProvider = messageColorProvider,
|
||||
emojiTypeFace = attributes.emojiTypeFace,
|
||||
itemClickListener = attributes.itemClickListener,
|
||||
itemLongClickListener = attributes.itemLongClickListener,
|
||||
reactionPillCallback = attributes.reactionPillCallback,
|
||||
readReceiptsCallback = attributes.readReceiptsCallback
|
||||
)
|
||||
)
|
||||
.highlighted(highlight)
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
}
|
||||
}
|
|
@ -16,16 +16,24 @@
|
|||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.factory
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.matrix.android.api.util.MatrixItem
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
|
||||
|
||||
abstract val attributes: Attributes
|
||||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
holder.expandView.setOnClickListener {
|
||||
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
|
||||
}
|
||||
if (attributes.isCollapsed) {
|
||||
holder.separatorView.visibility = View.GONE
|
||||
holder.expandView.setText(R.string.merged_events_expand)
|
||||
} else {
|
||||
holder.separatorView.visibility = View.VISIBLE
|
||||
holder.expandView.setText(R.string.merged_events_collapse)
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
protected val distinctMergeData by lazy {
|
||||
attributes.mergeData.distinctBy { it.userId }
|
||||
}
|
||||
|
||||
override fun getEventIds(): List<String> {
|
||||
return if (attributes.isCollapsed) {
|
||||
attributes.mergeData.map { it.eventId }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Data(
|
||||
val localId: Long,
|
||||
val eventId: String,
|
||||
val userId: String,
|
||||
val memberName: String,
|
||||
val avatarUrl: String?
|
||||
)
|
||||
|
||||
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
|
||||
|
||||
interface Attributes {
|
||||
val isCollapsed: Boolean
|
||||
val mergeData: List<Data>
|
||||
val avatarRenderer: AvatarRenderer
|
||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
|
||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
}
|
||||
|
||||
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
|
||||
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
|
||||
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
|
||||
}
|
||||
}
|
|
@ -24,28 +24,20 @@ import androidx.core.view.children
|
|||
import androidx.core.view.isVisible
|
||||
import 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
|
||||
}
|
|
@ -0,0 +1,115 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.riotx.features.home.room.detail.timeline.item
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.features.home.AvatarRenderer
|
||||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
|
||||
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
|
||||
|
||||
@EpoxyAttribute
|
||||
override lateinit var attributes: Attributes
|
||||
|
||||
override fun getViewType() = STUB_ID
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
|
||||
if (attributes.isCollapsed) {
|
||||
val data = distinctMergeData.firstOrNull()
|
||||
|
||||
val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
|
||||
data?.memberName ?: data?.userId ?: "")
|
||||
holder.summaryView.text = summary
|
||||
holder.summaryView.visibility = View.VISIBLE
|
||||
holder.avatarView.visibility = View.VISIBLE
|
||||
if (data != null) {
|
||||
holder.avatarView.visibility = View.VISIBLE
|
||||
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
|
||||
} else {
|
||||
holder.avatarView.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (attributes.hasEncryptionEvent) {
|
||||
holder.encryptionTile.isVisible = true
|
||||
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
|
||||
this.marginEnd = leftGuideline
|
||||
}
|
||||
if (attributes.isEncryptionAlgorithmSecure) {
|
||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
|
||||
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
|
||||
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
|
||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
|
||||
null, null, null
|
||||
)
|
||||
} else {
|
||||
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
|
||||
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
|
||||
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
|
||||
null, null, null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
holder.encryptionTile.isVisible = false
|
||||
}
|
||||
} else {
|
||||
holder.avatarView.visibility = View.INVISIBLE
|
||||
holder.summaryView.visibility = View.GONE
|
||||
holder.encryptionTile.isGone = true
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
|
||||
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
|
||||
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
|
||||
|
||||
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
|
||||
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val STUB_ID = R.id.messageContentMergedCreationStub
|
||||
}
|
||||
|
||||
data class Attributes(
|
||||
override val isCollapsed: Boolean,
|
||||
override val mergeData: List<Data>,
|
||||
override val avatarRenderer: AvatarRenderer,
|
||||
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
|
||||
override val onCollapsedStateChanged: (Boolean) -> Unit,
|
||||
val hasEncryptionEvent : Boolean,
|
||||
val isEncryptionAlgorithmSecure: Boolean
|
||||
) : BasedMergedItem.Attributes
|
||||
}
|
|
@ -31,7 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
|
|||
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
|
||||
|
||||
@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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/creationEncryptionTile"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginEnd="52dp"
|
||||
android:layout_marginBottom="2dp"
|
||||
android:background="@drawable/rounded_rect_shape_8"
|
||||
android:padding="8dp">
|
||||
|
||||
<include layout="@layout/item_timeline_event_status_tile_stub" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/mergedSumContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/creationEncryptionTile">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemNoticeAvatarView"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemNoticeTextView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_gravity="top"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_toStartOf="@id/itemMergedExpandTextView"
|
||||
android:layout_toEndOf="@id/itemNoticeAvatarView"
|
||||
android:textColor="?riotx_text_secondary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic"
|
||||
tools:text="@string/room_created_summary_item" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemMergedExpandTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="8dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:text="@string/merged_events_expand"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="italic" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<View
|
||||
android:id="@+id/itemMergedSeparatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@+id/mergedSumContainer"
|
||||
android:layout_marginTop="4dp"
|
||||
android:background="?attr/riotx_header_panel_background" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -87,6 +87,13 @@
|
|||
<string name="bootstrap_skip_text">Setting a Message Password lets you secure & unlock encrypted messages and trust.\n\nIf you don’t want to set a Message Password, generate a Message Key instead.</string>
|
||||
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure & unlock encrypted messages and trust.</string>
|
||||
|
||||
|
||||
<string name="encryption_enabled">Encryption enabled</string>
|
||||
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more & verify users in their profile.</string>
|
||||
<string name="encryption_not_enabled">Encryption not enabled</string>
|
||||
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
|
||||
|
||||
<string name="room_created_summary_item">%s created and configured the room.</string>
|
||||
<!-- END Strings added by Valere -->
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue