Timeline: handle filtering in epoxy

This commit is contained in:
ganfra 2021-03-29 21:05:25 +02:00
parent d6d4293ea8
commit 2b93367165
26 changed files with 431 additions and 206 deletions

View file

@ -16,6 +16,7 @@
package im.vector.app.core.epoxy package im.vector.app.core.epoxy
import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
@ -25,6 +26,14 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents { abstract class TimelineEmptyItem : VectorEpoxyModel<TimelineEmptyItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String @EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute var visible: Boolean = false
override fun bind(holder: Holder) {
super.bind(holder)
holder.view.updateLayoutParams {
this.height = if (visible) 1 else 0
}
}
override fun getEventIds(): List<String> { override fun getEventIds(): List<String> {
return listOf(eventId) return listOf(eventId)

View file

@ -66,7 +66,7 @@ class JumpToBottomViewVisibilityManager(
} }
private fun maybeShowJumpToBottomViewVisibility() { private fun maybeShowJumpToBottomViewVisibility() {
if (layoutManager.findFirstVisibleItemPosition() != 0) { if (layoutManager.findFirstVisibleItemPosition() > 1) {
jumpToBottomView.show() jumpToBottomView.show()
} else { } else {
jumpToBottomView.hide() jumpToBottomView.hide()

View file

@ -20,7 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.core.platform.DefaultListUpdateCallback
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import timber.log.Timber import org.matrix.android.sdk.api.extensions.tryOrNull
import java.util.concurrent.CopyOnWriteArrayList import java.util.concurrent.CopyOnWriteArrayList
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
@ -38,24 +38,27 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
} }
override fun onInserted(position: Int, count: Int) { override fun onInserted(position: Int, count: Int) {
if (position != 0) {
return
}
if (forceScroll) { if (forceScroll) {
forceScroll = false forceScroll = false
layoutManager.scrollToPosition(position) layoutManager.scrollToPosition(0)
return return
} }
Timber.v("On inserted $count count at position: $position") if (layoutManager.findFirstVisibleItemPosition() > 1) {
if (layoutManager.findFirstVisibleItemPosition() != position) {
return return
} }
val firstNewItem = timelineEventController.adapter.getModelAtPosition(position) as? ItemWithEvents ?: return val firstNewItem = tryOrNull {
timelineEventController.adapter.getModelAtPosition(position)
} as? ItemWithEvents ?: return
val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return val firstNewItemIds = firstNewItem.getEventIds().firstOrNull() ?: return
val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds) val indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
if (indexOfFirstNewItem != -1) { if (indexOfFirstNewItem != -1) {
Timber.v("Should scroll to position: $position") while (newTimelineEventIds.lastOrNull() != firstNewItemIds) {
repeat(newTimelineEventIds.size - indexOfFirstNewItem) { newTimelineEventIds.removeLastOrNull()
newTimelineEventIds.removeAt(indexOfFirstNewItem)
} }
layoutManager.scrollToPosition(position) layoutManager.scrollToPosition(0)
} }
} }
} }

View file

@ -31,16 +31,21 @@ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull import im.vector.app.core.extensions.prevOrNull
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiffUtilCallback
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
@ -49,6 +54,8 @@ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
@ -58,6 +65,7 @@ import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@ -65,8 +73,6 @@ import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject import javax.inject.Inject
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
@ -77,7 +83,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val session: Session, private val session: Session,
private val callManager: WebRtcCallManager, private val callManager: WebRtcCallManager,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler private val backgroundHandler: Handler,
private val userPreferencesProvider: UserPreferencesProvider,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
interface Callback : interface Callback :
@ -147,7 +156,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private var unreadState: UnreadState = UnreadState.Unknown private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null private var positionOfReadMarker: Int? = null
private var eventIdToHighlight: String? = null private var eventIdToHighlight: String? = null
private var previousModelsSize = 0
var callback: Callback? = null var callback: Callback? = null
var timeline: Timeline? = null var timeline: Timeline? = null
@ -198,7 +206,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val interceptorHelper = TimelineControllerInterceptorHelper( private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker, ::positionOfReadMarker,
adapterPositionMapping, adapterPositionMapping,
vectorPreferences, userPreferencesProvider,
callManager callManager
) )
@ -311,7 +319,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} else { } else {
cacheItemData.eventModel cacheItemData.eventModel
} }
listOf(eventModel, listOf(
cacheItemData?.readReceiptsItem?.takeIf { cacheItemData.mergedHeaderModel == null },
eventModel,
cacheItemData?.mergedHeaderModel, cacheItemData?.mergedHeaderModel,
cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null } cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null }
) )
@ -323,61 +333,94 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { private fun buildCacheItemsIfNeeded() = synchronized(modelCache) {
hasUTD = false hasUTD = false
hasReachedInvite = false hasReachedInvite = false
if (modelCache.isEmpty()) { if (modelCache.isEmpty()) {
return return
} }
val receiptsByEvents = getReadReceiptsByShownEvent()
(0 until modelCache.size).forEach { position -> (0 until modelCache.size).forEach { position ->
// Should be build if not cached or if cached but contains additional models val event = currentSnapshot[position]
// We then are sure we always have items up to date. val nextEvent = currentSnapshot.nextOrNull(position)
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) { val prevEvent = currentSnapshot.prevOrNull(position)
modelCache[position] = buildCacheItem(position, currentSnapshot) // Should be build if not cached or if model should be refreshed
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
modelCache[position] = buildCacheItem(event, nextEvent, prevEvent)
} }
val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed
modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
} }
} }
private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>): CacheItemData { private fun buildCacheItem(event: TimelineEvent,
val event = items[currentPosition] nextEvent: TimelineEvent?,
val nextEvent = items.nextOrNull(currentPosition) prevEvent: TimelineEvent?
val prevEvent = items.prevOrNull(currentPosition) ): CacheItemData {
if (hasReachedInvite && hasUTD) { if (hasReachedInvite && hasUTD) {
return CacheItemData(event.localId, event.root.eventId, null, null, null) return CacheItemData(event.localId, event.root.eventId)
} }
updateUTDStates(event, nextEvent) updateUTDStates(event, nextEvent)
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also { val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event)) it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
} }
val addDaySeparator = if (hasReachedInvite && hasUTD) { val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
true return CacheItemData(
} else { localId = event.localId,
val date = event.root.localDateTime() eventId = event.root.eventId,
val nextDate = nextEvent?.root?.localDateTime() eventModel = eventModel,
date.toLocalDate() != nextDate?.toLocalDate() shouldTriggerBuild = shouldTriggerBuild)
} }
private fun CacheItemData.enrichWithModels(event: TimelineEvent,
nextEvent: TimelineEvent?,
position: Int,
receiptsByEvents: Map<String, List<ReadReceipt>>): CacheItemData {
val wantsDateSeparator = wantsDateSeparator(event, nextEvent)
val mergedHeaderModel = mergedHeaderItemFactory.create(event, val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent, nextEvent = nextEvent,
items = items, items = this@TimelineEventController.currentSnapshot,
addDaySeparator = addDaySeparator, addDaySeparator = wantsDateSeparator,
currentPosition = currentPosition, currentPosition = position,
eventIdToHighlight = eventIdToHighlight, eventIdToHighlight = eventIdToHighlight,
callback = callback callback = callback
) { ) {
requestModelBuild() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs) val formattedDayModel = if (wantsDateSeparator) {
// If we have a SENT decoration, we want to built again as it might have to be changed to NONE if more recent event has also SENT decoration buildDaySeparatorItem(event.root.originServerTs)
val forceTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, forceTriggerBuild)
}
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
return if (addDaySeparator) {
val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
} else { } else {
null null
} }
val readReceipts = receiptsByEvents[event.eventId].orEmpty()
return copy(
readReceiptsItem = readReceiptsItemFactory.create(event.eventId, readReceipts, callback),
formattedDayModel = formattedDayModel,
mergedHeaderModel = mergedHeaderModel
)
}
private fun getReadReceiptsByShownEvent(): Map<String, List<ReadReceipt>> {
val receiptsByEvent = HashMap<String, MutableList<ReadReceipt>>()
var lastShownEventId: String? = null
val itr = currentSnapshot.listIterator(currentSnapshot.size)
while (itr.hasPrevious()) {
val event = itr.previous()
val currentReadReceipts = ArrayList(event.readReceipts)
if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
lastShownEventId = event.eventId
}
if (lastShownEventId == null) {
continue
}
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
existingReceipts.addAll(currentReadReceipts)
}
return receiptsByEvent
}
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
return DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
} }
private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ { private fun LoadingItem_.setVisibilityStateChangedListener(direction: Timeline.Direction): LoadingItem_ {
@ -409,6 +452,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
} }
} }
private fun wantsDateSeparator(event: TimelineEvent, nextEvent: TimelineEvent?): Boolean {
return if (hasReachedInvite && hasUTD) {
true
} else {
val date = event.root.localDateTime()
val nextDate = nextEvent?.root?.localDateTime()
date.toLocalDate() != nextDate?.toLocalDate()
}
}
/** /**
* Return true if added * Return true if added
*/ */
@ -429,14 +482,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private data class CacheItemData( private data class CacheItemData(
val localId: Long, val localId: Long,
val eventId: String?, val eventId: String?,
val readReceiptsItem: ReadReceiptsItem? = null,
val eventModel: EpoxyModel<*>? = null, val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null, val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null, val formattedDayModel: DaySeparatorItem? = null,
val forceTriggerBuild: Boolean = false val shouldTriggerBuild: Boolean = false
) { )
fun shouldTriggerBuild(): Boolean {
// Since those items can change when we paginate, force a re-build
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
}
}
} }

View file

@ -43,8 +43,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
text = text, text = text,
itemLongClickListener = { view -> itemLongClickListener = { view ->
callback?.onEventLongClicked(informationData, null, view) ?: false callback?.onEventLongClicked(informationData, null, view) ?: false
}, }
readReceiptsCallback = callback
) )
return DefaultItem_() return DefaultItem_()
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)

View file

@ -23,9 +23,9 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
import im.vector.app.features.home.room.detail.timeline.helper.prevSameTypeEvents
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem
import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_ import im.vector.app.features.home.room.detail.timeline.item.MergedMembershipEventsItem_
@ -47,7 +47,8 @@ import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder, class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider, private val avatarSizeProvider: AvatarSizeProvider,
private val roomSummariesHolder: RoomSummariesHolder) { private val roomSummariesHolder: RoomSummariesHolder,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf<Long>() private val collapsedEventIds = linkedSetOf<Long>()
private val mergeItemCollapseStates = HashMap<Long, Boolean>() private val mergeItemCollapseStates = HashMap<Long, Boolean>()
@ -85,7 +86,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
eventIdToHighlight: String?, eventIdToHighlight: String?,
requestModelBuild: () -> Unit, requestModelBuild: () -> Unit,
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) val prevSameTypeEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2)
return if (prevSameTypeEvents.isEmpty()) { return if (prevSameTypeEvents.isEmpty()) {
null null
} else { } else {
@ -126,8 +127,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
onCollapsedStateChanged = { onCollapsedStateChanged = {
mergeItemCollapseStates[event.localId] = it mergeItemCollapseStates[event.localId] = it
requestModelBuild() requestModelBuild()
}, }
readReceiptsCallback = callback
) )
MergedMembershipEventsItem_() MergedMembershipEventsItem_()
.id(mergeId) .id(mergeId)
@ -205,7 +205,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
}, },
hasEncryptionEvent = hasEncryption, hasEncryptionEvent = hasEncryption,
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM, isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
readReceiptsCallback = callback,
callback = callback, callback = callback,
currentUserId = currentUserId, currentUserId = currentUserId,
roomSummary = roomSummariesHolder.get(event.roomId), roomSummary = roomSummariesHolder.get(event.roomId),

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.factory
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem_
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import javax.inject.Inject
class ReadReceiptsItemFactory @Inject constructor(private val session: Session,
private val avatarRenderer: AvatarRenderer) {
fun create(eventId: String, readReceipts: List<ReadReceipt>, callback: TimelineEventController.Callback?): ReadReceiptsItem? {
val readReceiptsData = readReceipts
.asSequence()
.filter {
it.user.userId != session.myUserId
}
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList()
if (readReceiptsData.isEmpty()) {
return null
}
return ReadReceiptsItem_()
.id("read_receipts_$eventId")
.eventId(eventId)
.readReceipts(readReceiptsData)
.avatarRenderer(avatarRenderer)
.clickListener(DebouncedClickListener({ _ ->
callback?.onReadReceiptsClicked(readReceiptsData)
}))
}
}

View file

@ -21,6 +21,7 @@ import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import timber.log.Timber import timber.log.Timber
@ -35,7 +36,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val widgetItemFactory: WidgetItemFactory, private val widgetItemFactory: WidgetItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory, private val verificationConclusionItemFactory: VerificationItemFactory,
private val callItemFactory: CallItemFactory, private val callItemFactory: CallItemFactory,
private val userPreferencesProvider: UserPreferencesProvider) { private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
/** /**
* Reminder: nextEvent is older and prevEvent is newer. * Reminder: nextEvent is older and prevEvent is newer.
@ -46,12 +47,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
eventIdToHighlight: String?, eventIdToHighlight: String?,
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
val highlight = event.root.eventId == eventIdToHighlight val highlight = event.root.eventId == eventIdToHighlight
val computedModel = try { val computedModel = try {
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
return buildEmptyItem(event, prevEvent, eventIdToHighlight)
}
when (event.root.getClearType()) { when (event.root.getClearType()) {
// Message items
EventType.STICKER, EventType.STICKER,
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
// State and call
EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_TOMBSTONE,
EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_NAME,
EventType.STATE_ROOM_TOPIC, EventType.STATE_ROOM_TOPIC,
@ -63,8 +66,19 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
EventType.STATE_ROOM_HISTORY_VISIBILITY, EventType.STATE_ROOM_HISTORY_VISIBILITY,
EventType.STATE_ROOM_SERVER_ACL, EventType.STATE_ROOM_SERVER_ACL,
EventType.STATE_ROOM_GUEST_ACCESS, EventType.STATE_ROOM_GUEST_ACCESS,
EventType.STATE_ROOM_POWER_LEVELS, EventType.REDACTION ,
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE,
EventType.REACTION,
EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET_LEGACY,
EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback)
EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback)
@ -84,30 +98,10 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback) encryptedItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
} }
} }
EventType.STATE_ROOM_ALIASES,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_READY,
EventType.KEY_VERIFICATION_MAC,
EventType.REACTION,
EventType.CALL_CANDIDATES,
EventType.CALL_REPLACES,
EventType.CALL_SELECT_ANSWER,
EventType.CALL_NEGOTIATE -> {
// TODO These are not filtered out by timeline when encrypted
// For now manually ignore
if (userPreferencesProvider.shouldShowHiddenEvents()) {
noticeItemFactory.create(event, highlight, callback)
} else {
null
}
}
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> { EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback) verificationConclusionItemFactory.create(event, highlight, callback)
} }
// Unhandled event types // Unhandled event types
else -> { else -> {
// Should only happen when shouldShowHiddenEvents() settings is ON // Should only happen when shouldShowHiddenEvents() settings is ON
@ -119,12 +113,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
Timber.e(throwable, "failed to create message item") Timber.e(throwable, "failed to create message item")
defaultItemFactory.create(event, highlight, callback, throwable) defaultItemFactory.create(event, highlight, callback, throwable)
} }
return computedModel ?: buildEmptyItem(event) return computedModel ?: buildEmptyItem(event, prevEvent, eventIdToHighlight)
} }
private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem { private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, eventIdToHighlight: String?): TimelineEmptyItem {
val makesEmptyItemVisible = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, eventIdToHighlight)
return TimelineEmptyItem_() return TimelineEmptyItem_()
.id(timelineEvent.localId) .id(timelineEvent.localId)
.eventId(timelineEvent.eventId) .eventId(timelineEvent.eventId)
.visible(makesEmptyItemVisible)
} }
} }

View file

@ -111,15 +111,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
}, },
hasBeenEdited = event.hasBeenEdited(), hasBeenEdited = event.hasBeenEdited(),
hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false, hasPendingEdits = event.annotations?.editSummary?.localEchos?.any() ?: false,
readReceipts = event.readReceipts
.asSequence()
.filter {
it.user.userId != session.myUserId
}
.map {
ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs)
}
.toList(),
referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary -> referencesInfoData = event.annotations?.referencesAggregatedSummary?.let { referencesAggregatedSummary ->
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
?: VerificationState.REQUEST ?: VerificationState.REQUEST

View file

@ -19,11 +19,14 @@ package im.vector.app.features.home.room.detail.timeline.helper
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.epoxy.TimelineEmptyItem
import im.vector.app.core.epoxy.TimelineEmptyItem_ import im.vector.app.core.epoxy.TimelineEmptyItem_
import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.call.webrtc.WebRtcCallManager import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -34,7 +37,7 @@ private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>, class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0<Int?>,
private val adapterPositionMapping: MutableMap<String, Int>, private val adapterPositionMapping: MutableMap<String, Int>,
private val vectorPreferences: VectorPreferences, private val userPreferencesProvider: UserPreferencesProvider,
private val callManager: WebRtcCallManager private val callManager: WebRtcCallManager
) { ) {
@ -56,23 +59,40 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
models.addForwardPrefetchIfNeeded(timeline, callback) models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator() val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents() val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
var index = 0 var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
var atLeastOneVisibleItemSinceLastDaySeparator = false
var atLeastOneVisibleItemsBeforeReadMarker = false
// Then iterate on models so we have the exact positions in the adapter // Then iterate on models so we have the exact positions in the adapter
modelsIterator.forEach { epoxyModel -> modelsIterator.forEach { epoxyModel ->
if(epoxyModel !is TimelineEmptyItem){
atLeastOneVisibleItemSinceLastDaySeparator = true
atLeastOneVisibleItemsBeforeReadMarker = true
}
if (epoxyModel is ItemWithEvents) { if (epoxyModel is ItemWithEvents) {
epoxyModel.getEventIds().forEach { eventId -> epoxyModel.getEventIds().forEach { eventId ->
adapterPositionMapping[eventId] = index adapterPositionMapping[eventId] = index
if (eventId == firstUnreadEventId) { if (eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) {
modelsIterator.addReadMarkerItem(callback) modelsIterator.addReadMarkerItem(callback)
index++ index++
positionOfReadMarker.set(index) positionOfReadMarker.set(index)
} }
} }
} }
if (epoxyModel is CallTileTimelineItem) { if(epoxyModel is DaySeparatorItem){
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents) if(!atLeastOneVisibleItemSinceLastDaySeparator){
modelsIterator.remove()
return@forEach
}
atLeastOneVisibleItemSinceLastDaySeparator = false
}
else if (epoxyModel is CallTileTimelineItem) {
val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
if(!hasBeenRemoved){
atLeastOneVisibleItemSinceLastDaySeparator = true
}
} }
index++ index++
} }
@ -94,20 +114,23 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
epoxyModel: CallTileTimelineItem, epoxyModel: CallTileTimelineItem,
callIds: MutableSet<String>, callIds: MutableSet<String>,
showHiddenEvents: Boolean showHiddenEvents: Boolean
) { ): Boolean {
val callId = epoxyModel.attributes.callId val callId = epoxyModel.attributes.callId
// We should remove the call tile if we already have one for this call or // We should remove the call tile if we already have one for this call or
// if this is an active call tile without an actual call (which can happen with permalink) // if this is an active call tile without an actual call (which can happen with permalink)
val shouldRemoveCallItem = callIds.contains(callId) val shouldRemoveCallItem = callIds.contains(callId)
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive()) || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) { val removed = shouldRemoveCallItem && !showHiddenEvents
if (removed) {
remove() remove()
val emptyItem = TimelineEmptyItem_() val emptyItem = TimelineEmptyItem_()
.id(epoxyModel.id()) .id(epoxyModel.id())
.eventId(epoxyModel.attributes.informationData.eventId) .eventId(epoxyModel.attributes.informationData.eventId)
.visible(false)
add(emptyItem) add(emptyItem)
} }
callIds.add(callId) callIds.add(callId)
return removed
} }
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {

View file

@ -22,6 +22,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
object TimelineDisplayableEvents { object TimelineDisplayableEvents {
/**
* All types we have an item to build with. Every type not defined here will be shown as DefaultItem if forced to be shown, otherwise will be hidden.
*/
val DISPLAYABLE_TYPES = listOf( val DISPLAYABLE_TYPES = listOf(
EventType.MESSAGE, EventType.MESSAGE,
EventType.STATE_ROOM_WIDGET_LEGACY, EventType.STATE_ROOM_WIDGET_LEGACY,
@ -50,6 +53,7 @@ object TimelineDisplayableEvents {
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL EventType.KEY_VERIFICATION_CANCEL
) )
} }
fun TimelineEvent.canBeMerged(): Boolean { fun TimelineEvent.canBeMerged(): Boolean {
@ -68,7 +72,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
EventType.STATE_ROOM_CANONICAL_ALIAS, EventType.STATE_ROOM_CANONICAL_ALIAS,
EventType.STATE_ROOM_POWER_LEVELS, EventType.STATE_ROOM_POWER_LEVELS,
EventType.STATE_ROOM_ENCRYPTION -> true EventType.STATE_ROOM_ENCRYPTION -> true
EventType.STATE_ROOM_MEMBER -> { EventType.STATE_ROOM_MEMBER -> {
// Keep only room member events regarding the room creator (when he joined the room), // Keep only room member events regarding the room creator (when he joined the room),
// but exclude events where the room creator invite others, or where others join // but exclude events where the room creator invite others, or where others join
roomCreatorUserId != null && root.stateKey == roomCreatorUserId roomCreatorUserId != null && root.stateKey == roomCreatorUserId
@ -76,39 +80,3 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
else -> false else -> false
} }
} }
fun List<TimelineEvent>.nextSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
if (index >= size - 1) {
return emptyList()
}
val timelineEvent = this[index]
val nextSubList = subList(index + 1, size)
val indexOfNextDay = nextSubList.indexOfFirst {
val date = it.root.localDateTime()
val nextDate = timelineEvent.root.localDateTime()
date.toLocalDate() != nextDate.toLocalDate()
}
val nextSameDayEvents = if (indexOfNextDay == -1) {
nextSubList
} else {
nextSubList.subList(0, indexOfNextDay)
}
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
nextSameDayEvents
} else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
}
if (sameTypeEvents.size < minSize) {
return emptyList()
}
return sameTypeEvents
}
fun List<TimelineEvent>.prevSameTypeEvents(index: Int, minSize: Int): List<TimelineEvent> {
val prevSub = subList(0, index + 1)
return prevSub
.reversed()
.nextSameTypeEvents(0, minSize)
.reversed()
}

View file

@ -0,0 +1,131 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.helper
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.UserPreferencesProvider
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.RelationType
import org.matrix.android.sdk.api.session.events.model.getRelationContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import javax.inject.Inject
class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
fun nextSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int): List<TimelineEvent> {
if (index >= timelineEvents.size - 1) {
return emptyList()
}
val timelineEvent = timelineEvents[index]
val nextSubList = timelineEvents.subList(index + 1, timelineEvents.size)
val indexOfNextDay = nextSubList.indexOfFirst {
val date = it.root.localDateTime()
val nextDate = timelineEvent.root.localDateTime()
date.toLocalDate() != nextDate.toLocalDate()
}
val nextSameDayEvents = if (indexOfNextDay == -1) {
nextSubList
} else {
nextSubList.subList(0, indexOfNextDay)
}
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
nextSameDayEvents
} else {
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
}
val filteredSameTypeEvents = sameTypeEvents.filter { shouldShowEvent(it)}
if (filteredSameTypeEvents.size < minSize) {
return emptyList()
}
return filteredSameTypeEvents
}
fun prevSameTypeEvents(timelineEvents: List<TimelineEvent>, index: Int, minSize: Int): List<TimelineEvent> {
val prevSub = timelineEvents.subList(0, index + 1)
return prevSub
.reversed()
.let {
nextSameTypeEvents(it, 0, minSize)
}
.reversed()
}
fun shouldShowEvent(timelineEvent: TimelineEvent, highlightEventId: String? = null): Boolean {
// If show hidden events is true we should always display something
if (userPreferencesProvider.shouldShowHiddenEvents()) {
return true
}
// We always show highlighted event
if (timelineEvent.eventId == highlightEventId) {
return true
}
if (!timelineEvent.isDisplayable()) {
return false
}
// Check for special case where we should hide the event, like redacted, relation, memberships... according to user preferences.
return !timelineEvent.shouldBeHidden()
}
private fun TimelineEvent.isDisplayable(): Boolean {
return TimelineDisplayableEvents.DISPLAYABLE_TYPES.contains(root.getClearType())
}
private fun TimelineEvent.shouldBeHidden(): Boolean {
if (root.isRedacted() && !userPreferencesProvider.shouldShowRedactedMessages()) {
return true
}
if (root.getRelationContent()?.type == RelationType.REPLACE) {
return true
}
if (root.getClearType() == EventType.STATE_ROOM_MEMBER) {
val diff = computeMembershipDiff()
if ((diff.isJoin || diff.isPart) && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) return true
}
return false
}
private fun TimelineEvent.computeMembershipDiff(): MembershipDiff {
val content = root.getClearContent().toModel<RoomMemberContent>()
val prevContent = root.resolvedPrevContent().toModel<RoomMemberContent>()
val isMembershipChanged = content?.membership != prevContent?.membership;
val isJoin = isMembershipChanged && content?.membership == Membership.JOIN
val isPart = isMembershipChanged && content?.membership == Membership.LEAVE && root.stateKey == root.senderId
val isJoinToJoin = !isMembershipChanged && content?.membership == Membership.JOIN
val isDisplaynameChange = isJoinToJoin && content?.displayName != prevContent?.displayName;
val isAvatarChange = isJoinToJoin && content?.avatarUrl !== prevContent?.avatarUrl
return MembershipDiff(
isJoin = isJoin,
isPart = isPart,
isDisplaynameChange = isDisplaynameChange,
isAvatarChange = isAvatarChange
)
}
private data class MembershipDiff(
val isJoin: Boolean,
val isPart: Boolean,
val isDisplaynameChange: Boolean,
val isAvatarChange: Boolean
)
}

View file

@ -41,10 +41,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
abstract val baseAttributes: Attributes abstract val baseAttributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
})
private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener { private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
override fun onReacted(reactionButton: ReactionButton) { override fun onReacted(reactionButton: ReactionButton) {
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true) baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
@ -69,12 +65,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun bind(holder: H) { override fun bind(holder: H) {
super.bind(holder) super.bind(holder)
holder.readReceiptsView.render(
baseAttributes.informationData.readReceipts,
baseAttributes.avatarRenderer,
_readReceiptsClickListener
)
val reactions = baseAttributes.informationData.orderedReactionList val reactions = baseAttributes.informationData.orderedReactionList
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) { if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
holder.reactionsContainer.isVisible = false holder.reactionsContainer.isVisible = false
@ -111,7 +101,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
override fun unbind(holder: H) { override fun unbind(holder: H) {
holder.reactionsContainer.setOnLongClickListener(null) holder.reactionsContainer.setOnLongClickListener(null)
holder.readReceiptsView.unbind(baseAttributes.avatarRenderer)
super.unbind(holder) super.unbind(holder)
} }

View file

@ -56,7 +56,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
val leftGuideline by bind<View>(R.id.messageStartGuideline) val leftGuideline by bind<View>(R.id.messageStartGuideline)
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground) val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
override fun bindView(itemView: View) { override fun bindView(itemView: View) {
super.bindView(itemView) super.bindView(itemView)

View file

@ -41,8 +41,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
holder.separatorView.visibility = View.VISIBLE holder.separatorView.visibility = View.VISIBLE
holder.expandView.setText(R.string.merged_events_collapse) holder.expandView.setText(R.string.merged_events_collapse)
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
protected val distinctMergeData by lazy { protected val distinctMergeData by lazy {
@ -72,7 +70,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
val isCollapsed: Boolean val isCollapsed: Boolean
val mergeData: List<Data> val mergeData: List<Data>
val avatarRenderer: AvatarRenderer val avatarRenderer: AvatarRenderer
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
val onCollapsedStateChanged: (Boolean) -> Unit val onCollapsedStateChanged: (Boolean) -> Unit
} }

View file

@ -32,21 +32,15 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.messageTextView.text = attributes.text holder.messageTextView.text = attributes.text
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
} }
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
attributes.avatarRenderer.clear(holder.avatarImageView) attributes.avatarRenderer.clear(holder.avatarImageView)
holder.readReceiptsView.unbind(attributes.avatarRenderer)
super.unbind(holder) super.unbind(holder)
} }
@ -66,7 +60,6 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
val informationData: MessageInformationData, val informationData: MessageInformationData,
val text: CharSequence, val text: CharSequence,
val itemLongClickListener: View.OnLongClickListener? = null, val itemLongClickListener: View.OnLongClickListener? = null,
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
) )
companion object { companion object {

View file

@ -56,8 +56,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
holder.avatarListView.visibility = View.INVISIBLE holder.avatarListView.visibility = View.INVISIBLE
holder.summaryView.visibility = View.GONE holder.summaryView.visibility = View.GONE
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
class Holder : BasedMergedItem.Holder(STUB_ID) { class Holder : BasedMergedItem.Holder(STUB_ID) {
@ -73,7 +71,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
override val isCollapsed: Boolean, override val isCollapsed: Boolean,
override val mergeData: List<Data>, override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit override val onCollapsedStateChanged: (Boolean) -> Unit
) : BasedMergedItem.Attributes ) : BasedMergedItem.Attributes
} }

View file

@ -92,8 +92,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
holder.summaryView.visibility = View.GONE holder.summaryView.visibility = View.GONE
holder.encryptionTile.isGone = true holder.encryptionTile.isGone = true
} }
// No read receipt for this item
holder.readReceiptsView.isVisible = false
} }
private fun bindEncryptionTile(holder: Holder, data: Data?) { private fun bindEncryptionTile(holder: Holder, data: Data?) {
@ -223,7 +221,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
override val isCollapsed: Boolean, override val isCollapsed: Boolean,
override val mergeData: List<Data>, override val mergeData: List<Data>,
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null,
override val onCollapsedStateChanged: (Boolean) -> Unit, override val onCollapsedStateChanged: (Boolean) -> Unit,
val callback: TimelineEventController.Callback? = null, val callback: TimelineEventController.Callback? = null,
val currentUserId: String, val currentUserId: String,

View file

@ -36,10 +36,8 @@ data class MessageInformationData(
/*List of reactions (emoji,count,isSelected)*/ /*List of reactions (emoji,count,isSelected)*/
val orderedReactionList: List<ReactionInfoData>? = null, val orderedReactionList: List<ReactionInfoData>? = null,
val pollResponseAggregatedSummary: PollResponseData? = null, val pollResponseAggregatedSummary: PollResponseData? = null,
val hasBeenEdited: Boolean = false, val hasBeenEdited: Boolean = false,
val hasPendingEdits: Boolean = false, val hasPendingEdits: Boolean = false,
val readReceipts: List<ReadReceiptData> = emptyList(),
val referencesInfoData: ReferencesInfoData? = null, val referencesInfoData: ReferencesInfoData? = null,
val sentByMe: Boolean, val sentByMe: Boolean,
val e2eDecoration: E2EDecoration = E2EDecoration.NONE, val e2eDecoration: E2EDecoration = E2EDecoration.NONE,

View file

@ -36,16 +36,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var attributes: Attributes lateinit var attributes: Attributes
private val _readReceiptsClickListener = DebouncedClickListener({
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
})
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.noticeTextView.text = attributes.noticeText holder.noticeTextView.text = attributes.noticeText
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView) attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
holder.view.setOnLongClickListener(attributes.itemLongClickListener) holder.view.setOnLongClickListener(attributes.itemLongClickListener)
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
holder.avatarImageView.onClick(attributes.avatarClickListener) holder.avatarImageView.onClick(attributes.avatarClickListener)
when (attributes.informationData.e2eDecoration) { when (attributes.informationData.e2eDecoration) {
@ -62,7 +57,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
override fun unbind(holder: Holder) { override fun unbind(holder: Holder) {
attributes.avatarRenderer.clear(holder.avatarImageView) attributes.avatarRenderer.clear(holder.avatarImageView)
holder.readReceiptsView.unbind(attributes.avatarRenderer)
super.unbind(holder) super.unbind(holder)
} }

View file

@ -0,0 +1,51 @@
/*
* Copyright (c) 2021 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.app.features.home.room.detail.timeline.item
import android.view.View
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.ui.views.ReadReceiptsView
import im.vector.app.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_timeline_event_read_receipts)
abstract class ReadReceiptsItem : EpoxyModelWithHolder<ReadReceiptsItem.Holder>(), ItemWithEvents {
@EpoxyAttribute lateinit var eventId: String
@EpoxyAttribute lateinit var readReceipts: List<ReadReceiptData>
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: View.OnClickListener
override fun getEventIds(): List<String> = listOf(eventId)
override fun bind(holder: Holder) {
super.bind(holder)
holder.readReceiptsView.render(readReceipts, avatarRenderer, clickListener)
}
override fun unbind(holder: Holder) {
holder.readReceiptsView.unbind(avatarRenderer)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
}
}

View file

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<View xmlns:android="http://schemas.android.com/apk/res/android" <View xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" /> android:layout_height="0dp" />

View file

@ -188,15 +188,6 @@
android:layout_height="wrap_content" /--> android:layout_height="wrap_content" /-->
</com.google.android.flexbox.FlexboxLayout> </com.google.android.flexbox.FlexboxLayout>
<im.vector.app.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -10,7 +10,7 @@
android:id="@+id/messageSelectedBackground" android:id="@+id/messageSelectedBackground"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignBottom="@+id/readReceiptsView" android:layout_alignParentBottom="true"
android:layout_alignParentTop="true" android:layout_alignParentTop="true"
android:background="@drawable/highlighted_message_background" /> android:background="@drawable/highlighted_message_background" />
@ -80,14 +80,4 @@
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
<im.vector.app.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/viewStubContainer"
android:layout_alignParentEnd="true"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</RelativeLayout> </RelativeLayout>

View file

@ -120,14 +120,6 @@
</com.google.android.flexbox.FlexboxLayout> </com.google.android.flexbox.FlexboxLayout>
<im.vector.app.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</LinearLayout> </LinearLayout>
</RelativeLayout> </RelativeLayout>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<im.vector.app.core.ui.views.ReadReceiptsView
android:id="@+id/readReceiptsView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="8dp"
android:layout_marginBottom="4dp" />
</FrameLayout>