From 2b933671659ba85323c7ef56e203f41779ffd664 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 29 Mar 2021 21:05:25 +0200 Subject: [PATCH] Timeline: handle filtering in epoxy --- .../app/core/epoxy/TimelineEmptyItem.kt | 9 ++ .../JumpToBottomViewVisibilityManager.kt | 2 +- .../room/detail/ScrollOnNewMessageCallback.kt | 21 +-- .../timeline/TimelineEventController.kt | 135 ++++++++++++------ .../timeline/factory/DefaultItemFactory.kt | 3 +- .../factory/MergedHeaderItemFactory.kt | 11 +- .../factory/ReadReceiptsItemFactory.kt | 55 +++++++ .../timeline/factory/TimelineItemFactory.kt | 50 +++---- .../helper/MessageInformationDataFactory.kt | 9 -- .../TimelineControllerInterceptorHelper.kt | 37 ++++- .../helper/TimelineDisplayableEvents.kt | 42 +----- .../helper/TimelineEventVisibilityHelper.kt | 131 +++++++++++++++++ .../timeline/item/AbsBaseMessageItem.kt | 11 -- .../detail/timeline/item/BaseEventItem.kt | 1 - .../detail/timeline/item/BasedMergedItem.kt | 3 - .../room/detail/timeline/item/DefaultItem.kt | 7 - .../item/MergedMembershipEventsItem.kt | 3 - .../timeline/item/MergedRoomCreationItem.kt | 3 - .../timeline/item/MessageInformationData.kt | 2 - .../room/detail/timeline/item/NoticeItem.kt | 6 - .../detail/timeline/item/ReadReceiptsItem.kt | 51 +++++++ .../main/res/layout/item_timeline_empty.xml | 2 +- .../res/layout/item_timeline_event_base.xml | 9 -- .../item_timeline_event_base_noinfo.xml | 12 +- .../layout/item_timeline_event_base_state.xml | 8 -- .../item_timeline_event_read_receipts.xml | 14 ++ 26 files changed, 431 insertions(+), 206 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt create mode 100644 vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt create mode 100644 vector/src/main/res/layout/item_timeline_event_read_receipts.xml diff --git a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt index b77670ba76..9c49a5d458 100644 --- a/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt +++ b/vector/src/main/java/im/vector/app/core/epoxy/TimelineEmptyItem.kt @@ -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(), 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 { return listOf(eventId) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt index 2810b27aa6..7c0dcbb0d2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/JumpToBottomViewVisibilityManager.kt @@ -66,7 +66,7 @@ class JumpToBottomViewVisibilityManager( } private fun maybeShowJumpToBottomViewVisibility() { - if (layoutManager.findFirstVisibleItemPosition() != 0) { + if (layoutManager.findFirstVisibleItemPosition() > 1) { jumpToBottomView.show() } else { jumpToBottomView.hide() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt index fbf9ebe32f..249618e12f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnNewMessageCallback.kt @@ -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) } } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt index 44f1e9b759..972736fb2a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt @@ -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): 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>): 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> { + val receiptsByEvent = HashMap>() + 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 + ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt index 71ac46307b..5f5d3f5156 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/DefaultItemFactory.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt index 2134645d8d..4e4a7fce02 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt @@ -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() private val mergeItemCollapseStates = HashMap() @@ -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), diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt new file mode 100644 index 0000000000..a3e8541b05 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt @@ -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, 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) + })) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt index 73f101d1f5..df4eab0efe 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactory.kt @@ -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) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 09f173de14..abaa2ffa17 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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()?.verificationState ?: VerificationState.REQUEST diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt index 971a3a35d8..392cb0ae57 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt @@ -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, private val adapterPositionMapping: MutableMap, - 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, 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>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt index eb5b8081f9..a597fb966e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineDisplayableEvents.kt @@ -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.nextSameTypeEvents(index: Int, minSize: Int): List { - 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.prevSameTypeEvents(index: Int, minSize: Int): List { - val prevSub = subList(0, index + 1) - return prevSub - .reversed() - .nextSameTypeEvents(0, minSize) - .reversed() -} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt new file mode 100644 index 0000000000..54d76bc681 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -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, index: Int, minSize: Int): List { + 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, index: Int, minSize: Int): List { + 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() + val prevContent = root.resolvedPrevContent().toModel() + + 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 + ) +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt index a65f1e10f2..a5f3f7c547 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsBaseMessageItem.kt @@ -41,10 +41,6 @@ abstract class AbsBaseMessageItem : 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 : 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 : BaseEventItem override fun unbind(holder: H) { holder.reactionsContainer.setOnLongClickListener(null) - holder.readReceiptsView.unbind(baseAttributes.avatarRenderer) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt index 13bb6db6ef..aae1edbed1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BaseEventItem.kt @@ -56,7 +56,6 @@ abstract class BaseEventItem : VectorEpoxyModel abstract class BaseHolder(@IdRes val stubId: Int) : VectorEpoxyHolder() { val leftGuideline by bind(R.id.messageStartGuideline) val checkableBackground by bind(R.id.messageSelectedBackground) - val readReceiptsView by bind(R.id.readReceiptsView) override fun bindView(itemView: View) { super.bindView(itemView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt index 1f8ad3df1b..8a49bd6803 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/BasedMergedItem.kt @@ -41,8 +41,6 @@ abstract class BasedMergedItem : BaseEventItem() 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 : BaseEventItem() val isCollapsed: Boolean val mergeData: List val avatarRenderer: AvatarRenderer - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? val onCollapsedStateChanged: (Boolean) -> Unit } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt index cdc677334e..580a56bf05 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/DefaultItem.kt @@ -32,21 +32,15 @@ abstract class DefaultItem : BaseEventItem() { @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() { val informationData: MessageInformationData, val text: CharSequence, val itemLongClickListener: View.OnLongClickListener? = null, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null ) companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt index ef4a6662b4..70a6864a1f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedMembershipEventsItem.kt @@ -56,8 +56,6 @@ abstract class MergedMembershipEventsItem : BasedMergedItem, override val avatarRenderer: AvatarRenderer, - override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val onCollapsedStateChanged: (Boolean) -> Unit ) : BasedMergedItem.Attributes } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt index 6a665bb44f..9faef589ca 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MergedRoomCreationItem.kt @@ -92,8 +92,6 @@ abstract class MergedRoomCreationItem : BasedMergedItem, override val avatarRenderer: AvatarRenderer, - override val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null, override val onCollapsedStateChanged: (Boolean) -> Unit, val callback: TimelineEventController.Callback? = null, val currentUserId: String, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt index 67b79bab9b..08aa301538 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/MessageInformationData.kt @@ -36,10 +36,8 @@ data class MessageInformationData( /*List of reactions (emoji,count,isSelected)*/ val orderedReactionList: List? = null, val pollResponseAggregatedSummary: PollResponseData? = null, - val hasBeenEdited: Boolean = false, val hasPendingEdits: Boolean = false, - val readReceipts: List = emptyList(), val referencesInfoData: ReferencesInfoData? = null, val sentByMe: Boolean, val e2eDecoration: E2EDecoration = E2EDecoration.NONE, diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt index bcf170dc4d..b733d0ec32 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/NoticeItem.kt @@ -36,16 +36,11 @@ abstract class NoticeItem : BaseEventItem() { @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() { override fun unbind(holder: Holder) { attributes.avatarRenderer.clear(holder.avatarImageView) - holder.readReceiptsView.unbind(attributes.avatarRenderer) super.unbind(holder) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt new file mode 100644 index 0000000000..84b2662687 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt @@ -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(), ItemWithEvents { + + @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute lateinit var readReceipts: List + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) lateinit var clickListener: View.OnClickListener + + override fun getEventIds(): List = 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(R.id.readReceiptsView) + } +} diff --git a/vector/src/main/res/layout/item_timeline_empty.xml b/vector/src/main/res/layout/item_timeline_empty.xml index c8dee60cc7..562cbd39ba 100644 --- a/vector/src/main/res/layout/item_timeline_empty.xml +++ b/vector/src/main/res/layout/item_timeline_empty.xml @@ -1,4 +1,4 @@ \ No newline at end of file + android:layout_height="0dp" /> diff --git a/vector/src/main/res/layout/item_timeline_event_base.xml b/vector/src/main/res/layout/item_timeline_event_base.xml index ce3460a21c..f9562f65b0 100644 --- a/vector/src/main/res/layout/item_timeline_event_base.xml +++ b/vector/src/main/res/layout/item_timeline_event_base.xml @@ -188,15 +188,6 @@ android:layout_height="wrap_content" /--> - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml index 6442f230d5..35e1b097d7 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_noinfo.xml @@ -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" /> - - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_base_state.xml b/vector/src/main/res/layout/item_timeline_event_base_state.xml index db5ed052f3..98cea901da 100644 --- a/vector/src/main/res/layout/item_timeline_event_base_state.xml +++ b/vector/src/main/res/layout/item_timeline_event_base_state.xml @@ -120,14 +120,6 @@ - - \ No newline at end of file diff --git a/vector/src/main/res/layout/item_timeline_event_read_receipts.xml b/vector/src/main/res/layout/item_timeline_event_read_receipts.xml new file mode 100644 index 0000000000..f741e434c7 --- /dev/null +++ b/vector/src/main/res/layout/item_timeline_event_read_receipts.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file