mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-03-18 12:18:48 +03:00
Timeline: handle filtering in epoxy
This commit is contained in:
parent
d6d4293ea8
commit
2b93367165
26 changed files with 431 additions and 206 deletions
|
@ -16,6 +16,7 @@
|
|||
|
||||
package im.vector.app.core.epoxy
|
||||
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import com.airbnb.epoxy.EpoxyAttribute
|
||||
import com.airbnb.epoxy.EpoxyModelClass
|
||||
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 {
|
||||
|
||||
@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> {
|
||||
return listOf(eventId)
|
||||
|
|
|
@ -66,7 +66,7 @@ class JumpToBottomViewVisibilityManager(
|
|||
}
|
||||
|
||||
private fun maybeShowJumpToBottomViewVisibility() {
|
||||
if (layoutManager.findFirstVisibleItemPosition() != 0) {
|
||||
if (layoutManager.findFirstVisibleItemPosition() > 1) {
|
||||
jumpToBottomView.show()
|
||||
} else {
|
||||
jumpToBottomView.hide()
|
||||
|
|
|
@ -20,7 +20,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
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.item.ItemWithEvents
|
||||
import timber.log.Timber
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
||||
|
@ -38,24 +38,27 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager,
|
|||
}
|
||||
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
if (position != 0) {
|
||||
return
|
||||
}
|
||||
if (forceScroll) {
|
||||
forceScroll = false
|
||||
layoutManager.scrollToPosition(position)
|
||||
layoutManager.scrollToPosition(0)
|
||||
return
|
||||
}
|
||||
Timber.v("On inserted $count count at position: $position")
|
||||
if (layoutManager.findFirstVisibleItemPosition() != position) {
|
||||
if (layoutManager.findFirstVisibleItemPosition() > 1) {
|
||||
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 indexOfFirstNewItem = newTimelineEventIds.indexOf(firstNewItemIds)
|
||||
if (indexOfFirstNewItem != -1) {
|
||||
Timber.v("Should scroll to position: $position")
|
||||
repeat(newTimelineEventIds.size - indexOfFirstNewItem) {
|
||||
newTimelineEventIds.removeAt(indexOfFirstNewItem)
|
||||
while (newTimelineEventIds.lastOrNull() != firstNewItemIds) {
|
||||
newTimelineEventIds.removeLastOrNull()
|
||||
}
|
||||
layoutManager.scrollToPosition(position)
|
||||
layoutManager.scrollToPosition(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,16 +31,21 @@ import im.vector.app.core.epoxy.LoadingItem_
|
|||
import im.vector.app.core.extensions.localDateTime
|
||||
import im.vector.app.core.extensions.nextOrNull
|
||||
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.home.AvatarRenderer
|
||||
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.UnreadState
|
||||
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.helper.ContentDownloadStateTrackerBinder
|
||||
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.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.TimelineMediaSizeProvider
|
||||
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.MessageInformationData
|
||||
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.url.PreviewUrlRetriever
|
||||
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.toModel
|
||||
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.message.MessageImageInfoContent
|
||||
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 javax.inject.Inject
|
||||
|
||||
private const val DEFAULT_PREFETCH_THRESHOLD = 30
|
||||
|
||||
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
|
||||
|
@ -77,7 +83,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private val session: Session,
|
||||
private val callManager: WebRtcCallManager,
|
||||
@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 {
|
||||
|
||||
interface Callback :
|
||||
|
@ -147,7 +156,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private var unreadState: UnreadState = UnreadState.Unknown
|
||||
private var positionOfReadMarker: Int? = null
|
||||
private var eventIdToHighlight: String? = null
|
||||
private var previousModelsSize = 0
|
||||
|
||||
var callback: Callback? = null
|
||||
var timeline: Timeline? = null
|
||||
|
@ -198,7 +206,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private val interceptorHelper = TimelineControllerInterceptorHelper(
|
||||
::positionOfReadMarker,
|
||||
adapterPositionMapping,
|
||||
vectorPreferences,
|
||||
userPreferencesProvider,
|
||||
callManager
|
||||
)
|
||||
|
||||
|
@ -311,7 +319,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
} else {
|
||||
cacheItemData.eventModel
|
||||
}
|
||||
listOf(eventModel,
|
||||
listOf(
|
||||
cacheItemData?.readReceiptsItem?.takeIf { cacheItemData.mergedHeaderModel == null },
|
||||
eventModel,
|
||||
cacheItemData?.mergedHeaderModel,
|
||||
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) {
|
||||
hasUTD = false
|
||||
hasReachedInvite = false
|
||||
|
||||
if (modelCache.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val receiptsByEvents = getReadReceiptsByShownEvent()
|
||||
(0 until modelCache.size).forEach { position ->
|
||||
// Should be build if not cached or if cached but contains additional models
|
||||
// We then are sure we always have items up to date.
|
||||
if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) {
|
||||
modelCache[position] = buildCacheItem(position, currentSnapshot)
|
||||
val event = currentSnapshot[position]
|
||||
val nextEvent = currentSnapshot.nextOrNull(position)
|
||||
val prevEvent = currentSnapshot.prevOrNull(position)
|
||||
// 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 {
|
||||
val event = items[currentPosition]
|
||||
val nextEvent = items.nextOrNull(currentPosition)
|
||||
val prevEvent = items.prevOrNull(currentPosition)
|
||||
private fun buildCacheItem(event: TimelineEvent,
|
||||
nextEvent: TimelineEvent?,
|
||||
prevEvent: TimelineEvent?
|
||||
): CacheItemData {
|
||||
if (hasReachedInvite && hasUTD) {
|
||||
return CacheItemData(event.localId, event.root.eventId, null, null, null)
|
||||
return CacheItemData(event.localId, event.root.eventId)
|
||||
}
|
||||
updateUTDStates(event, nextEvent)
|
||||
val eventModel = timelineItemFactory.create(event, prevEvent, nextEvent, eventIdToHighlight, callback).also {
|
||||
it.id(event.localId)
|
||||
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
|
||||
}
|
||||
val addDaySeparator = if (hasReachedInvite && hasUTD) {
|
||||
true
|
||||
} else {
|
||||
val date = event.root.localDateTime()
|
||||
val nextDate = nextEvent?.root?.localDateTime()
|
||||
date.toLocalDate() != nextDate?.toLocalDate()
|
||||
}
|
||||
val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
|
||||
return CacheItemData(
|
||||
localId = event.localId,
|
||||
eventId = event.root.eventId,
|
||||
eventModel = eventModel,
|
||||
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,
|
||||
nextEvent = nextEvent,
|
||||
items = items,
|
||||
addDaySeparator = addDaySeparator,
|
||||
currentPosition = currentPosition,
|
||||
items = this@TimelineEventController.currentSnapshot,
|
||||
addDaySeparator = wantsDateSeparator,
|
||||
currentPosition = position,
|
||||
eventIdToHighlight = eventIdToHighlight,
|
||||
callback = callback
|
||||
) {
|
||||
requestModelBuild()
|
||||
}
|
||||
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
|
||||
// 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
|
||||
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)
|
||||
val formattedDayModel = if (wantsDateSeparator) {
|
||||
buildDaySeparatorItem(event.root.originServerTs)
|
||||
} else {
|
||||
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_ {
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -429,14 +482,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
|
|||
private data class CacheItemData(
|
||||
val localId: Long,
|
||||
val eventId: String?,
|
||||
val readReceiptsItem: ReadReceiptsItem? = null,
|
||||
val eventModel: EpoxyModel<*>? = null,
|
||||
val mergedHeaderModel: BasedMergedItem<*>? = null,
|
||||
val formattedDayModel: DaySeparatorItem? = null,
|
||||
val forceTriggerBuild: Boolean = false
|
||||
) {
|
||||
fun shouldTriggerBuild(): Boolean {
|
||||
// Since those items can change when we paginate, force a re-build
|
||||
return forceTriggerBuild || mergedHeaderModel != null || formattedDayModel != null
|
||||
}
|
||||
}
|
||||
val shouldTriggerBuild: Boolean = false
|
||||
)
|
||||
}
|
||||
|
|
|
@ -43,8 +43,7 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava
|
|||
text = text,
|
||||
itemLongClickListener = { view ->
|
||||
callback?.onEventLongClicked(informationData, null, view) ?: false
|
||||
},
|
||||
readReceiptsCallback = callback
|
||||
}
|
||||
)
|
||||
return DefaultItem_()
|
||||
.leftGuideline(avatarSizeProvider.leftGuideline)
|
||||
|
|
|
@ -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.MergedTimelineEventVisibilityStateChangedListener
|
||||
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.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.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,
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val avatarSizeProvider: AvatarSizeProvider,
|
||||
private val roomSummariesHolder: RoomSummariesHolder) {
|
||||
private val roomSummariesHolder: RoomSummariesHolder,
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
|
@ -85,7 +86,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
eventIdToHighlight: String?,
|
||||
requestModelBuild: () -> Unit,
|
||||
callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? {
|
||||
val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2)
|
||||
val prevSameTypeEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2)
|
||||
return if (prevSameTypeEvents.isEmpty()) {
|
||||
null
|
||||
} else {
|
||||
|
@ -126,8 +127,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
onCollapsedStateChanged = {
|
||||
mergeItemCollapseStates[event.localId] = it
|
||||
requestModelBuild()
|
||||
},
|
||||
readReceiptsCallback = callback
|
||||
}
|
||||
)
|
||||
MergedMembershipEventsItem_()
|
||||
.id(mergeId)
|
||||
|
@ -205,7 +205,6 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde
|
|||
},
|
||||
hasEncryptionEvent = hasEncryption,
|
||||
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
|
||||
readReceiptsCallback = callback,
|
||||
callback = callback,
|
||||
currentUserId = currentUserId,
|
||||
roomSummary = roomSummariesHolder.get(event.roomId),
|
||||
|
|
|
@ -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)
|
||||
}))
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import im.vector.app.core.epoxy.TimelineEmptyItem_
|
|||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
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.helper.TimelineEventVisibilityHelper
|
||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||
import timber.log.Timber
|
||||
|
@ -35,7 +36,7 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
private val widgetItemFactory: WidgetItemFactory,
|
||||
private val verificationConclusionItemFactory: VerificationItemFactory,
|
||||
private val callItemFactory: CallItemFactory,
|
||||
private val userPreferencesProvider: UserPreferencesProvider) {
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
|
||||
|
||||
/**
|
||||
* Reminder: nextEvent is older and prevEvent is newer.
|
||||
|
@ -46,12 +47,14 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
eventIdToHighlight: String?,
|
||||
callback: TimelineEventController.Callback?): VectorEpoxyModel<*> {
|
||||
val highlight = event.root.eventId == eventIdToHighlight
|
||||
|
||||
val computedModel = try {
|
||||
if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
|
||||
return buildEmptyItem(event, prevEvent, eventIdToHighlight)
|
||||
}
|
||||
when (event.root.getClearType()) {
|
||||
// Message items
|
||||
EventType.STICKER,
|
||||
EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback)
|
||||
// State and call
|
||||
EventType.STATE_ROOM_TOMBSTONE,
|
||||
EventType.STATE_ROOM_NAME,
|
||||
EventType.STATE_ROOM_TOPIC,
|
||||
|
@ -63,8 +66,19 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
|
|||
EventType.STATE_ROOM_HISTORY_VISIBILITY,
|
||||
EventType.STATE_ROOM_SERVER_ACL,
|
||||
EventType.STATE_ROOM_GUEST_ACCESS,
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
EventType.REDACTION -> noticeItemFactory.create(event, highlight, callback)
|
||||
EventType.REDACTION ,
|
||||
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 -> widgetItemFactory.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)
|
||||
}
|
||||
}
|
||||
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_DONE -> {
|
||||
verificationConclusionItemFactory.create(event, highlight, callback)
|
||||
}
|
||||
|
||||
// Unhandled event types
|
||||
else -> {
|
||||
// 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")
|
||||
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_()
|
||||
.id(timelineEvent.localId)
|
||||
.eventId(timelineEvent.eventId)
|
||||
.visible(makesEmptyItemVisible)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,15 +111,6 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|
|||
},
|
||||
hasBeenEdited = event.hasBeenEdited(),
|
||||
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 ->
|
||||
val verificationState = referencesAggregatedSummary.content.toModel<ReferencesAggregatedContent>()?.verificationState
|
||||
?: VerificationState.REQUEST
|
||||
|
|
|
@ -19,11 +19,14 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
|||
import com.airbnb.epoxy.EpoxyModel
|
||||
import com.airbnb.epoxy.VisibilityState
|
||||
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.resources.UserPreferencesProvider
|
||||
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.timeline.TimelineEventController
|
||||
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.TimelineReadMarkerItem_
|
||||
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?>,
|
||||
private val adapterPositionMapping: MutableMap<String, Int>,
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val userPreferencesProvider: UserPreferencesProvider,
|
||||
private val callManager: WebRtcCallManager
|
||||
) {
|
||||
|
||||
|
@ -56,23 +59,40 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
|||
models.addForwardPrefetchIfNeeded(timeline, callback)
|
||||
|
||||
val modelsIterator = models.listIterator()
|
||||
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
|
||||
val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
|
||||
var index = 0
|
||||
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
|
||||
modelsIterator.forEach { epoxyModel ->
|
||||
if(epoxyModel !is TimelineEmptyItem){
|
||||
atLeastOneVisibleItemSinceLastDaySeparator = true
|
||||
atLeastOneVisibleItemsBeforeReadMarker = true
|
||||
}
|
||||
if (epoxyModel is ItemWithEvents) {
|
||||
epoxyModel.getEventIds().forEach { eventId ->
|
||||
adapterPositionMapping[eventId] = index
|
||||
if (eventId == firstUnreadEventId) {
|
||||
if (eventId == firstUnreadEventId && atLeastOneVisibleItemsBeforeReadMarker) {
|
||||
modelsIterator.addReadMarkerItem(callback)
|
||||
index++
|
||||
positionOfReadMarker.set(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (epoxyModel is CallTileTimelineItem) {
|
||||
modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
|
||||
if(epoxyModel is DaySeparatorItem){
|
||||
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++
|
||||
}
|
||||
|
@ -94,20 +114,23 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
|
|||
epoxyModel: CallTileTimelineItem,
|
||||
callIds: MutableSet<String>,
|
||||
showHiddenEvents: Boolean
|
||||
) {
|
||||
): Boolean {
|
||||
val callId = epoxyModel.attributes.callId
|
||||
// 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)
|
||||
val shouldRemoveCallItem = callIds.contains(callId)
|
||||
|| (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
|
||||
if (shouldRemoveCallItem && !showHiddenEvents) {
|
||||
val removed = shouldRemoveCallItem && !showHiddenEvents
|
||||
if (removed) {
|
||||
remove()
|
||||
val emptyItem = TimelineEmptyItem_()
|
||||
.id(epoxyModel.id())
|
||||
.eventId(epoxyModel.attributes.informationData.eventId)
|
||||
.visible(false)
|
||||
add(emptyItem)
|
||||
}
|
||||
callIds.add(callId)
|
||||
return removed
|
||||
}
|
||||
|
||||
private fun MutableList<EpoxyModel<*>>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
|
||||
|
|
|
@ -22,6 +22,9 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
|||
|
||||
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(
|
||||
EventType.MESSAGE,
|
||||
EventType.STATE_ROOM_WIDGET_LEGACY,
|
||||
|
@ -50,6 +53,7 @@ object TimelineDisplayableEvents {
|
|||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
|
@ -68,7 +72,7 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
|||
EventType.STATE_ROOM_CANONICAL_ALIAS,
|
||||
EventType.STATE_ROOM_POWER_LEVELS,
|
||||
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),
|
||||
// but exclude events where the room creator invite others, or where others join
|
||||
roomCreatorUserId != null && root.stateKey == roomCreatorUserId
|
||||
|
@ -76,39 +80,3 @@ fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
|||
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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -41,10 +41,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
|
||||
abstract val baseAttributes: Attributes
|
||||
|
||||
private val _readReceiptsClickListener = DebouncedClickListener({
|
||||
baseAttributes.readReceiptsCallback?.onReadReceiptsClicked(baseAttributes.informationData.readReceipts)
|
||||
})
|
||||
|
||||
private var reactionClickListener: ReactionButton.ReactedListener = object : ReactionButton.ReactedListener {
|
||||
override fun onReacted(reactionButton: ReactionButton) {
|
||||
baseAttributes.reactionPillCallback?.onClickOnReactionPill(baseAttributes.informationData, reactionButton.reactionString, true)
|
||||
|
@ -69,12 +65,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
|
||||
override fun bind(holder: H) {
|
||||
super.bind(holder)
|
||||
holder.readReceiptsView.render(
|
||||
baseAttributes.informationData.readReceipts,
|
||||
baseAttributes.avatarRenderer,
|
||||
_readReceiptsClickListener
|
||||
)
|
||||
|
||||
val reactions = baseAttributes.informationData.orderedReactionList
|
||||
if (!shouldShowReactionAtBottom() || reactions.isNullOrEmpty()) {
|
||||
holder.reactionsContainer.isVisible = false
|
||||
|
@ -111,7 +101,6 @@ abstract class AbsBaseMessageItem<H : AbsBaseMessageItem.Holder> : BaseEventItem
|
|||
|
||||
override fun unbind(holder: H) {
|
||||
holder.reactionsContainer.setOnLongClickListener(null)
|
||||
holder.readReceiptsView.unbind(baseAttributes.avatarRenderer)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ abstract class BaseEventItem<H : BaseEventItem.BaseHolder> : VectorEpoxyModel<H>
|
|||
abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() {
|
||||
val leftGuideline by bind<View>(R.id.messageStartGuideline)
|
||||
val checkableBackground by bind<CheckableView>(R.id.messageSelectedBackground)
|
||||
val readReceiptsView by bind<ReadReceiptsView>(R.id.readReceiptsView)
|
||||
|
||||
override fun bindView(itemView: View) {
|
||||
super.bindView(itemView)
|
||||
|
|
|
@ -41,8 +41,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
|||
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 {
|
||||
|
@ -72,7 +70,6 @@ abstract class BasedMergedItem<H : BasedMergedItem.Holder> : BaseEventItem<H>()
|
|||
val isCollapsed: Boolean
|
||||
val mergeData: List<Data>
|
||||
val avatarRenderer: AvatarRenderer
|
||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback?
|
||||
val onCollapsedStateChanged: (Boolean) -> Unit
|
||||
}
|
||||
|
||||
|
|
|
@ -32,21 +32,15 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
private val _readReceiptsClickListener = DebouncedClickListener({
|
||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||
})
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.messageTextView.text = attributes.text
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
attributes.avatarRenderer.clear(holder.avatarImageView)
|
||||
holder.readReceiptsView.unbind(attributes.avatarRenderer)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
|
@ -66,7 +60,6 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
|
|||
val informationData: MessageInformationData,
|
||||
val text: CharSequence,
|
||||
val itemLongClickListener: View.OnLongClickListener? = null,
|
||||
val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -56,8 +56,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
|||
holder.avatarListView.visibility = View.INVISIBLE
|
||||
holder.summaryView.visibility = View.GONE
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
class Holder : BasedMergedItem.Holder(STUB_ID) {
|
||||
|
@ -73,7 +71,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem<MergedMembershipEven
|
|||
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
|
||||
}
|
||||
|
|
|
@ -92,8 +92,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
holder.summaryView.visibility = View.GONE
|
||||
holder.encryptionTile.isGone = true
|
||||
}
|
||||
// No read receipt for this item
|
||||
holder.readReceiptsView.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindEncryptionTile(holder: Holder, data: Data?) {
|
||||
|
@ -223,7 +221,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem<MergedRoomCreationItem.H
|
|||
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 callback: TimelineEventController.Callback? = null,
|
||||
val currentUserId: String,
|
||||
|
|
|
@ -36,10 +36,8 @@ data class MessageInformationData(
|
|||
/*List of reactions (emoji,count,isSelected)*/
|
||||
val orderedReactionList: List<ReactionInfoData>? = null,
|
||||
val pollResponseAggregatedSummary: PollResponseData? = null,
|
||||
|
||||
val hasBeenEdited: Boolean = false,
|
||||
val hasPendingEdits: Boolean = false,
|
||||
val readReceipts: List<ReadReceiptData> = emptyList(),
|
||||
val referencesInfoData: ReferencesInfoData? = null,
|
||||
val sentByMe: Boolean,
|
||||
val e2eDecoration: E2EDecoration = E2EDecoration.NONE,
|
||||
|
|
|
@ -36,16 +36,11 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||
@EpoxyAttribute
|
||||
lateinit var attributes: Attributes
|
||||
|
||||
private val _readReceiptsClickListener = DebouncedClickListener({
|
||||
attributes.readReceiptsCallback?.onReadReceiptsClicked(attributes.informationData.readReceipts)
|
||||
})
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
holder.noticeTextView.text = attributes.noticeText
|
||||
attributes.avatarRenderer.render(attributes.informationData.matrixItem, holder.avatarImageView)
|
||||
holder.view.setOnLongClickListener(attributes.itemLongClickListener)
|
||||
holder.readReceiptsView.render(attributes.informationData.readReceipts, attributes.avatarRenderer, _readReceiptsClickListener)
|
||||
holder.avatarImageView.onClick(attributes.avatarClickListener)
|
||||
|
||||
when (attributes.informationData.e2eDecoration) {
|
||||
|
@ -62,7 +57,6 @@ abstract class NoticeItem : BaseEventItem<NoticeItem.Holder>() {
|
|||
|
||||
override fun unbind(holder: Holder) {
|
||||
attributes.avatarRenderer.clear(holder.avatarImageView)
|
||||
holder.readReceiptsView.unbind(attributes.avatarRenderer)
|
||||
super.unbind(holder)
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<View xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp" />
|
||||
android:layout_height="0dp" />
|
||||
|
|
|
@ -188,15 +188,6 @@
|
|||
android:layout_height="wrap_content" /-->
|
||||
|
||||
</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>
|
||||
|
||||
</RelativeLayout>
|
|
@ -10,7 +10,7 @@
|
|||
android:id="@+id/messageSelectedBackground"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_alignBottom="@+id/readReceiptsView"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@drawable/highlighted_message_background" />
|
||||
|
||||
|
@ -80,14 +80,4 @@
|
|||
android:visibility="gone"
|
||||
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>
|
|
@ -120,14 +120,6 @@
|
|||
|
||||
</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>
|
||||
|
||||
</RelativeLayout>
|
|
@ -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>
|
Loading…
Add table
Reference in a new issue