Merge pull request #1213 from vector-im/feature/timeline_sum_item

Feature/timeline sum item
This commit is contained in:
Valere 2020-04-08 17:08:53 +02:00 committed by GitHub
commit 3968bb3488
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 536 additions and 78 deletions

View file

@ -6,12 +6,14 @@ Features ✨:
- Cross-Signing | Verify new session from existing session (#1134)
- Cross-Signing | Bootstraping cross signing with 4S from mobile (#985)
Improvements 🙌:
- Verification DM / Handle concurrent .start after .ready (#794)
- Cross-Signing | Update Shield Logic for DM (#963)
- Cross-Signing | Complete security new session design update (#1135)
- Cross-Signing | Setup key backup as part of SSSS bootstrapping (#1201)
- Cross-Signing | Gossip key backup recovery key (#1200)
- Show room encryption status as a bubble tile (#1078)
Bugfix 🐛:
- Missing avatar/displayname after verification request message (#841)

View file

@ -47,9 +47,9 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVi
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
import im.vector.riotx.features.home.room.detail.timeline.item.BaseEventItem
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.riotx.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
@ -373,7 +373,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val localId: Long,
val eventId: String?,
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: MergedHeaderItem? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null
) {
fun shouldTriggerBuild(): Boolean {

View file

@ -0,0 +1,80 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.R
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_
import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val messageColorProvider: MessageColorProvider,
private val stringProvider: StringProvider,
private val informationDataFactory: MessageInformationDataFactory,
private val avatarSizeProvider: AvatarSizeProvider) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?): StatusTileTimelineItem? {
val algorithm = event.root.getClearContent().toModel<EncryptionEventContent>()?.algorithm
val informationData = informationDataFactory.create(event, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM
val title: String
val description: String
val shield: StatusTileTimelineItem.ShieldUIState
if (isSafeAlgorithm) {
title = stringProvider.getString(R.string.encryption_enabled)
description = stringProvider.getString(R.string.encryption_enabled_tile_description)
shield = StatusTileTimelineItem.ShieldUIState.BLACK
} else {
title = stringProvider.getString(R.string.encryption_not_enabled)
description = stringProvider.getString(R.string.encryption_unknown_algorithm_tile_description)
shield = StatusTileTimelineItem.ShieldUIState.RED
}
return StatusTileTimelineItem_()
.attributes(
StatusTileTimelineItem.Attributes(
title = title,
description = description,
shieldUIState = shield,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = messageColorProvider,
emojiTypeFace = attributes.emojiTypeFace,
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}

View file

@ -16,16 +16,24 @@
package im.vector.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM
import im.vector.matrix.android.internal.crypto.model.event.EncryptionEventContent
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.riotx.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.riotx.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.riotx.features.home.room.detail.timeline.helper.prevSameTypeEvents
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedHeaderItem_
import im.vector.riotx.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem
import im.vector.riotx.features.home.room.detail.timeline.item.MergedRoomCreationItem_
import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: ActiveSessionHolder,
@ -43,8 +51,12 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
eventIdToHighlight: String?,
callback: TimelineEventController.Callback?,
requestModelBuild: () -> Unit)
: MergedHeaderItem? {
return if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
: BasedMergedItem<*>? {
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE && event.isRoomConfiguration()) {
// It's the first item before room.create
// Collapse all room configuration events
buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
null
} else {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
@ -53,14 +65,14 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
} else {
var highlighted = false
val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed()
val mergedData = ArrayList<MergedHeaderItem.Data>(mergedEvents.size)
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = MergedHeaderItem.Data(
val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName,
@ -82,7 +94,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedHeaderItem.Attributes(
val attributes = MergedMembershipEventsItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
@ -92,7 +104,7 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
},
readReceiptsCallback = callback
)
MergedHeaderItem_()
MergedMembershipEventsItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
@ -104,6 +116,81 @@ class MergedHeaderItemFactory @Inject constructor(private val sessionHolder: Act
}
}
private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List<TimelineEvent>,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedRoomCreationItem_? {
var prevEvent = if (currentPosition > 0) items[currentPosition - 1] else null
var tmpPos = currentPosition - 1
val mergedEvents = ArrayList<TimelineEvent>().also { it.add(event) }
var hasEncryption = false
var encryptionAlgorithm: String? = null
while (prevEvent != null && prevEvent.isRoomConfiguration()) {
if (prevEvent.root.getClearType() == EventType.STATE_ROOM_ENCRYPTION) {
hasEncryption = true
encryptionAlgorithm = prevEvent.root.getClearContent()?.toModel<EncryptionEventContent>()?.algorithm
}
mergedEvents.add(prevEvent)
tmpPos--
prevEvent = if (tmpPos >= 0) items[tmpPos] else null
}
return if (mergedEvents.size > 2) {
var highlighted = false
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
mergedEvents.reversed()
.forEach { mergedEvent ->
if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) {
highlighted = true
}
val senderAvatar = mergedEvent.senderAvatar
val senderName = mergedEvent.getDisambiguatedDisplayName()
val data = BasedMergedItem.Data(
userId = mergedEvent.root.senderId ?: "",
avatarUrl = senderAvatar,
memberName = senderName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: ""
)
mergedData.add(data)
}
val mergedEventIds = mergedEvents.map { it.localId }
// We try to find if one of the item id were used as mergeItemCollapseStates key
// => handle case where paginating from mergeable events and we get more
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
val initialCollapseState = mergeItemCollapseStates.remove(previousCollapseStateKey)
?: true
val isCollapsed = mergeItemCollapseStates.getOrPut(event.localId) { initialCollapseState }
if (isCollapsed) {
collapsedEventIds.addAll(mergedEventIds)
} else {
collapsedEventIds.removeAll(mergedEventIds)
}
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
val attributes = MergedRoomCreationItem.Attributes(
isCollapsed = isCollapsed,
mergeData = mergedData,
avatarRenderer = avatarRenderer,
onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it
requestModelBuild()
},
hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback
)
MergedRoomCreationItem_()
.id(mergeId)
.leftGuideline(avatarSizeProvider.leftGuideline)
.highlighted(isCollapsed && highlighted)
.attributes(attributes)
.also {
it.setOnVisibilityStateChanged(MergedTimelineEventVisibilityStateChangedListener(callback, mergedEvents))
}
} else null
}
fun isCollapsed(localId: Long): Boolean {
return collapsedEventIds.contains(localId)
}

View file

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

View file

@ -25,15 +25,17 @@ import im.vector.matrix.android.api.session.room.model.message.MessageRelationCo
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem
import im.vector.riotx.features.home.room.detail.timeline.item.StatusTileTimelineItem_
import javax.inject.Inject
/**
@ -48,6 +50,7 @@ class VerificationItemFactory @Inject constructor(
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory,
private val userPreferencesProvider: UserPreferencesProvider,
private val stringProvider: StringProvider,
private val session: Session
) {
@ -88,12 +91,12 @@ class VerificationItemFactory @Inject constructor(
CancelCode.MismatchedKeys,
CancelCode.MismatchedSas -> {
// We should display these bad conclusions
return VerificationRequestConclusionItem_()
return StatusTileTimelineItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = false,
StatusTileTimelineItem.Attributes(
title = stringProvider.getString(R.string.verification_conclusion_warning),
description = "${informationData.memberName} (${informationData.senderId})",
shieldUIState = StatusTileTimelineItem.ShieldUIState.RED,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = messageColorProvider,
@ -121,12 +124,12 @@ class VerificationItemFactory @Inject constructor(
// We only display the done sent by the other user, the done send by me is ignored
return ignoredConclusion(event, highlight, callback)
}
return VerificationRequestConclusionItem_()
return StatusTileTimelineItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = true,
StatusTileTimelineItem.Attributes(
title = stringProvider.getString(R.string.sas_verified),
description = "${informationData.memberName} (${informationData.senderId})",
shieldUIState = StatusTileTimelineItem.ShieldUIState.GREEN,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
messageColorProvider = messageColorProvider,

View file

@ -50,6 +50,18 @@ fun TimelineEvent.canBeMerged(): Boolean {
return root.getClearType() == EventType.STATE_ROOM_MEMBER
}
fun TimelineEvent.isRoomConfiguration(): Boolean {
return when (root.getClearType()) {
EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_JOIN_RULES,
EventType.STATE_ROOM_MEMBER,
EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_ENCRYPTION -> true
else -> false
}
}
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
if (index >= size - 1) {
return emptyList()

View file

@ -0,0 +1,82 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.widget.TextView
import androidx.annotation.IdRes
import androidx.core.view.isVisible
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
abstract val attributes: Attributes
override fun bind(holder: H) {
super.bind(holder)
holder.expandView.setOnClickListener {
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
}
if (attributes.isCollapsed) {
holder.separatorView.visibility = View.GONE
holder.expandView.setText(R.string.merged_events_expand)
} else {
holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse)
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
protected val distinctMergeData by lazy {
attributes.mergeData.distinctBy { it.userId }
}
override fun getEventIds(): List<String> {
return if (attributes.isCollapsed) {
attributes.mergeData.map { it.eventId }
} else {
emptyList()
}
}
data class Data(
val localId: Long,
val eventId: String,
val userId: String,
val memberName: String,
val avatarUrl: String?
)
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
interface Attributes {
val isCollapsed: Boolean
val mergeData: List<Data>
val avatarRenderer: AvatarRenderer
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
val onCollapsedStateChanged: (Boolean) -> Unit
}
abstract class Holder(@IdRes stubId: Int) : BaseEventItem.BaseHolder(stubId) {
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
}
}

View file

@ -24,28 +24,20 @@ import androidx.core.view.children
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val distinctMergeData by lazy {
attributes.mergeData.distinctBy { it.userId }
}
abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEventsItem.Holder>() {
override fun getViewType() = STUB_ID
@EpoxyAttribute
override lateinit var attributes: Attributes
override fun bind(holder: Holder) {
super.bind(holder)
holder.expandView.setOnClickListener {
attributes.onCollapsedStateChanged(!attributes.isCollapsed)
}
if (attributes.isCollapsed) {
val summary = holder.expandView.resources.getQuantityString(R.plurals.membership_changes, attributes.mergeData.size, attributes.mergeData.size)
holder.summaryView.text = summary
@ -60,52 +52,28 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
view.visibility = View.GONE
}
}
holder.separatorView.visibility = View.GONE
holder.expandView.setText(R.string.merged_events_expand)
} else {
holder.avatarListView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse)
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
override fun getEventIds(): List<String> {
return if (attributes.isCollapsed) {
attributes.mergeData.map { it.eventId }
} else {
emptyList()
}
}
data class Data(
val localId: Long,
val eventId: String,
val userId: String,
val memberName: String,
val avatarUrl: String?
)
fun Data.toMatrixItem() = MatrixItem.UserItem(userId, memberName, avatarUrl)
data class Attributes(
val isCollapsed: Boolean,
val mergeData: List<Data>,
val avatarRenderer: AvatarRenderer,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val onCollapsedStateChanged: (Boolean) -> Unit
)
class Holder : BaseHolder(STUB_ID) {
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
val separatorView by bind<View>(R.id.itemMergedSeparatorView)
val avatarListView by bind<ViewGroup>(R.id.itemMergedAvatarListView)
}
companion object {
private const val STUB_ID = R.id.messageContentMergedHeaderStub
}
data class Attributes(
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes
}

View file

@ -0,0 +1,115 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.home.room.detail.timeline.item
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo)
abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.Holder>() {
@EpoxyAttribute
override lateinit var attributes: Attributes
override fun getViewType() = STUB_ID
override fun bind(holder: Holder) {
super.bind(holder)
if (attributes.isCollapsed) {
val data = distinctMergeData.firstOrNull()
val summary = holder.expandView.resources.getString(R.string.room_created_summary_item,
data?.memberName ?: data?.userId ?: "")
holder.summaryView.text = summary
holder.summaryView.visibility = View.VISIBLE
holder.avatarView.visibility = View.VISIBLE
if (data != null) {
holder.avatarView.visibility = View.VISIBLE
attributes.avatarRenderer.render(data.toMatrixItem(), holder.avatarView)
} else {
holder.avatarView.visibility = View.GONE
}
if (attributes.hasEncryptionEvent) {
holder.encryptionTile.isVisible = true
holder.encryptionTile.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
if (attributes.isEncryptionAlgorithmSecure) {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_enabled_tile_description)
holder.e2eTitleDescriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_black),
null, null, null
)
} else {
holder.e2eTitleTextView.text = holder.expandView.resources.getString(R.string.encryption_not_enabled)
holder.e2eTitleDescriptionView.text = holder.expandView.resources.getString(R.string.encryption_unknown_algorithm_tile_description)
holder.e2eTitleTextView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, R.drawable.ic_shield_warning),
null, null, null
)
}
} else {
holder.encryptionTile.isVisible = false
}
} else {
holder.avatarView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE
holder.encryptionTile.isGone = true
}
// No read receipt for this item
holder.readReceiptsView.isVisible = false
}
class Holder : BasedMergedItem.Holder(STUB_ID) {
val summaryView by bind<TextView>(R.id.itemNoticeTextView)
val avatarView by bind<ImageView>(R.id.itemNoticeAvatarView)
val encryptionTile by bind<ViewGroup>(R.id.creationEncryptionTile)
val e2eTitleTextView by bind<TextView>(R.id.itemVerificationDoneTitleTextView)
val e2eTitleDescriptionView by bind<TextView>(R.id.itemVerificationDoneDetailTextView)
}
companion object {
private const val STUB_ID = R.id.messageContentMergedCreationStub
}
data class Attributes(
override val isCollapsed: Boolean,
override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit,
val hasEncryptionEvent : Boolean,
val isEncryptionAlgorithmSecure: Boolean
) : BasedMergedItem.Attributes
}

View file

@ -31,7 +31,7 @@ import im.vector.riotx.features.home.room.detail.timeline.MessageColorProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<VerificationRequestConclusionItem.Holder>() {
abstract class StatusTileTimelineItem : AbsBaseMessageItem<StatusTileTimelineItem.Holder>() {
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
@ -47,11 +47,17 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
holder.endGuideline.updateLayoutParams<RelativeLayout.LayoutParams> {
this.marginEnd = leftGuideline
}
val title = if (attributes.isPositive) R.string.sas_verified else R.string.verification_conclusion_warning
holder.titleView.text = holder.view.context.getString(title)
holder.descriptionView.text = "${attributes.informationData.memberName} (${attributes.informationData.senderId})"
val startDrawable = if (attributes.isPositive) R.drawable.ic_shield_trusted else R.drawable.ic_shield_warning
holder.titleView.text = attributes.title
holder.descriptionView.text = attributes.description
holder.descriptionView.textAlignment = View.TEXT_ALIGNMENT_CENTER
val startDrawable = when (attributes.shieldUIState) {
ShieldUIState.GREEN -> R.drawable.ic_shield_trusted
ShieldUIState.BLACK -> R.drawable.ic_shield_black
ShieldUIState.RED -> R.drawable.ic_shield_warning
}
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(
ContextCompat.getDrawable(holder.view.context, startDrawable),
null, null, null
@ -75,9 +81,9 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
* This class holds all the common attributes for timeline items.
*/
data class Attributes(
val toUserId: String,
val toUserName: String,
val isPositive: Boolean,
val shieldUIState: ShieldUIState,
val title: CharSequence,
val description: CharSequence,
override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer,
override val messageColorProvider: MessageColorProvider,
@ -87,4 +93,10 @@ abstract class VerificationRequestConclusionItem : AbsBaseMessageItem<Verificati
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
val emojiTypeFace: Typeface? = null
) : AbsBaseMessageItem.Attributes
enum class ShieldUIState {
BLACK,
RED,
GREEN
}
}

View file

@ -54,6 +54,13 @@
tools:layout_marginTop="160dp"
tools:visibility="visible" />
<ViewStub
android:id="@+id/messageContentMergedCreationStub"
style="@style/TimelineContentStubBaseParams"
android:layout="@layout/item_timeline_event_merged_room_creation_stub"
tools:layout_marginTop="160dp"
tools:visibility="visible" />
</FrameLayout>
<im.vector.riotx.core.ui.views.ReadReceiptsView

View file

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

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:id="@+id/creationEncryptionTile"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_gravity="center"
android:layout_marginTop="2dp"
android:layout_marginEnd="52dp"
android:layout_marginBottom="2dp"
android:background="@drawable/rounded_rect_shape_8"
android:padding="8dp">
<include layout="@layout/item_timeline_event_status_tile_stub" />
</FrameLayout>
<RelativeLayout
android:id="@+id/mergedSumContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/creationEncryptionTile">
<ImageView
android:id="@+id/itemNoticeAvatarView"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/itemNoticeTextView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_gravity="top"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:layout_toStartOf="@id/itemMergedExpandTextView"
android:layout_toEndOf="@id/itemNoticeAvatarView"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
android:textStyle="italic"
tools:text="@string/room_created_summary_item" />
<TextView
android:id="@+id/itemMergedExpandTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginTop="2dp"
android:paddingLeft="8dp"
android:paddingTop="4dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:text="@string/merged_events_expand"
android:textColor="?attr/colorAccent"
android:textSize="14sp"
android:textStyle="italic" />
</RelativeLayout>
<View
android:id="@+id/itemMergedSeparatorView"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_below="@+id/mergedSumContainer"
android:layout_marginTop="4dp"
android:background="?attr/riotx_header_panel_background" />
</RelativeLayout>

View file

@ -87,6 +87,13 @@
<string name="bootstrap_skip_text">Setting a Message Password lets you secure &amp; unlock encrypted messages and trust.\n\nIf you dont want to set a Message Password, generate a Message Key instead.</string>
<string name="bootstrap_skip_text_no_gen_key">Setting a Message Password lets you secure &amp; unlock encrypted messages and trust.</string>
<string name="encryption_enabled">Encryption enabled</string>
<string name="encryption_enabled_tile_description">Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile.</string>
<string name="encryption_not_enabled">Encryption not enabled</string>
<string name="encryption_unknown_algorithm_tile_description">The encryption used by this room is not supported</string>
<string name="room_created_summary_item">%s created and configured the room.</string>
<!-- END Strings added by Valere -->