This commit is contained in:
Valere 2020-04-07 13:49:39 +02:00
parent 0eff00ebee
commit 68512e475f
5 changed files with 152 additions and 46 deletions

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

@ -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<TimelineEvent>().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<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 = 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<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 +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,

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,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<H : BasedMergedItem.Holder> : BaseEventItem<H>() {
@EpoxyAttribute
lateinit var attributes: Attributes
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)
data class Attributes(
val isCollapsed: Boolean,
val mergeData: List<Data>,
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<ViewGroup>(R.id.reactionsContainer)
}
}

View file

@ -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<MergedHeaderItem.Holder>() {
@EpoxyAttribute
lateinit var attributes: Attributes
private val distinctMergeData by lazy {
attributes.mergeData.distinctBy { it.userId }
}
abstract class MergedHeaderItem : BasedMergedItem<MergedHeaderItem.Holder>() {
override fun getViewType() = STUB_ID
@ -72,33 +61,7 @@ abstract class MergedHeaderItem : BaseEventItem<MergedHeaderItem.Holder>() {
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) {
class Holder : BasedMergedItem.Holder(STUB_ID) {
val expandView by bind<TextView>(R.id.itemMergedExpandTextView)
val summaryView by bind<TextView>(R.id.itemMergedSummaryTextView)
val separatorView by bind<View>(R.id.itemMergedSeparatorView)