diff --git a/CHANGES.md b/CHANGES.md index 0559c31b90..ed549c918f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -13,6 +13,7 @@ Improvements 🙌: - Api interceptor to allow app developers peek responses (#2986) - Update reactions to Unicode 13.1 (#2998) - Be more robust when parsing some enums + - Improve timeline filtering (dissociate membership and profile events, display hidden events when highlighted, fix hidden item/read receipts behavior) Bugfix 🐛: - Fix bad theme change for the MainActivity @@ -24,6 +25,7 @@ Translations 🗣: SDK API changes ⚠️: - Several Services have been migrated to coroutines (#2449) + - Removes filtering options on Timeline. Build 🧱: - diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt index 8932d0734e..06c88db831 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/Timeline.kt @@ -95,12 +95,6 @@ interface Timeline { */ fun getTimelineEventWithId(eventId: String?): TimelineEvent? - /** - * Returns the first displayable events starting from eventId. - * It does depend on the provided [TimelineSettings]. - */ - fun getFirstDisplayableEventId(eventId: String): String? - interface Listener { /** * Call when the timeline has been updated through pagination or sync. diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt index 25c63d6fbc..ceffedb234 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/timeline/TimelineSettings.kt @@ -24,10 +24,6 @@ data class TimelineSettings( * The initial number of events to retrieve from cache. You might get less events if you don't have loaded enough yet. */ val initialSize: Int, - /** - * Filters for timeline event - */ - val filters: TimelineEventFilters = TimelineEventFilters(), /** * If true, will build read receipts for each event. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt index a2b36ce590..f3bea68c26 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/TimelineEventMapper.kt @@ -17,7 +17,6 @@ package org.matrix.android.sdk.internal.database.mapper import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.sender.SenderInfo import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.database.model.TimelineEventEntity @@ -25,9 +24,9 @@ import javax.inject.Inject internal class TimelineEventMapper @Inject constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper) { - fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true, correctedReadReceipts: List? = null): TimelineEvent { + fun map(timelineEventEntity: TimelineEventEntity, buildReadReceipts: Boolean = true): TimelineEvent { val readReceipts = if (buildReadReceipts) { - correctedReadReceipts ?: timelineEventEntity.readReceipts + timelineEventEntity.readReceipts ?.let { readReceiptsSummaryMapper.map(it) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt index 61f770b956..1ed142ce23 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimeline.kt @@ -28,7 +28,6 @@ import org.matrix.android.sdk.api.NoOpMatrixCallback import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.session.events.model.EventType -import org.matrix.android.sdk.api.session.room.model.ReadReceipt import org.matrix.android.sdk.api.session.room.send.SendState import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent @@ -70,14 +69,12 @@ internal class DefaultTimeline( private val paginationTask: PaginationTask, private val timelineEventMapper: TimelineEventMapper, private val settings: TimelineSettings, - private val hiddenReadReceipts: TimelineHiddenReadReceipts, private val timelineInput: TimelineInput, private val eventDecryptor: TimelineEventDecryptor, private val realmSessionProvider: RealmSessionProvider, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler ) : Timeline, - TimelineHiddenReadReceipts.Delegate, TimelineInput.Listener, UIEchoManager.Listener { @@ -93,8 +90,7 @@ internal class DefaultTimeline( private val cancelableBag = CancelableBag() private val debouncer = Debouncer(mainHandler) - private lateinit var nonFilteredEvents: RealmResults - private lateinit var filteredEvents: RealmResults + private lateinit var timelineEvents: RealmResults private lateinit var sendingEvents: RealmResults private var prevDisplayIndex: Int? = null @@ -168,16 +164,9 @@ internal class DefaultTimeline( postSnapshot() } - nonFilteredEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() - filteredEvents = nonFilteredEvents.where() - .filterEventsWithSettings(settings) - .findAll() - nonFilteredEvents.addChangeListener(eventsChangeListener) + timelineEvents = buildEventQuery(realm).sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING).findAll() + timelineEvents.addChangeListener(eventsChangeListener) handleInitialLoad() - if (settings.shouldHandleHiddenReadReceipts()) { - hiddenReadReceipts.start(realm, filteredEvents, nonFilteredEvents, this) - } - loadRoomMembersTask .configureWith(LoadRoomMembersTask.Params(roomId)) { this.callback = NoOpMatrixCallback() @@ -205,10 +194,6 @@ internal class DefaultTimeline( } } - private fun TimelineSettings.shouldHandleHiddenReadReceipts(): Boolean { - return buildReadReceipts && (filters.filterEdits || filters.filterTypes) - } - override fun dispose() { if (isStarted.compareAndSet(true, false)) { isReady.set(false) @@ -220,11 +205,8 @@ internal class DefaultTimeline( if (this::sendingEvents.isInitialized) { sendingEvents.removeAllChangeListeners() } - if (this::nonFilteredEvents.isInitialized) { - nonFilteredEvents.removeAllChangeListeners() - } - if (settings.shouldHandleHiddenReadReceipts()) { - hiddenReadReceipts.dispose() + if (this::timelineEvents.isInitialized) { + timelineEvents.removeAllChangeListeners() } clearAllValues() backgroundRealm.getAndSet(null).also { @@ -256,48 +238,6 @@ internal class DefaultTimeline( } } - override fun getFirstDisplayableEventId(eventId: String): String? { - // If the item is built, the id is obviously displayable - val builtIndex = builtEventsIdMap[eventId] - if (builtIndex != null) { - return eventId - } - // Otherwise, we should check if the event is in the db, but is hidden because of filters - return realmSessionProvider.withRealm { localRealm -> - val nonFilteredEvents = buildEventQuery(localRealm) - .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) - .findAll() - - val nonFilteredEvent = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() - - val filteredEvents = nonFilteredEvents.where() - .filterEventsWithSettings(settings) - .findAll() - val isEventInDb = nonFilteredEvent != null - - val isHidden = isEventInDb && filteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() == null - - if (isHidden) { - val displayIndex = nonFilteredEvent?.displayIndex - if (displayIndex != null) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) - .findFirst() - firstDisplayedEvent?.eventId - } else { - null - } - } else { - null - } - } - } - override fun hasMoreToLoad(direction: Timeline.Direction): Boolean { return hasMoreInCache(direction) || !hasReachedEnd(direction) } @@ -319,18 +259,6 @@ internal class DefaultTimeline( listeners.clear() } -// TimelineHiddenReadReceipts.Delegate - - override fun rebuildEvent(eventId: String, readReceipts: List): Boolean { - return rebuildEvent(eventId) { te -> - te.copy(readReceipts = readReceipts) - } - } - - override fun onReadReceiptsUpdated() { - postSnapshot() - } - override fun onNewTimelineEvents(roomId: String, eventIds: List) { if (isLive && this.roomId == roomId) { listeners.forEach { @@ -341,18 +269,13 @@ internal class DefaultTimeline( override fun onLocalEchoCreated(roomId: String, timelineEvent: TimelineEvent) { if (roomId != this.roomId || !isLive) return - - val postSnapShot = uiEchoManager.onLocalEchoCreated(timelineEvent) - - if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { - listeners.forEach { + uiEchoManager.onLocalEchoCreated(timelineEvent) + listeners.forEach { + tryOrNull { it.onNewTimelineEvents(listOf(timelineEvent.eventId)) } } - - if (postSnapShot) { - postSnapshot() - } + postSnapshot() } override fun onLocalEchoUpdated(roomId: String, eventId: String, sendState: SendState) { @@ -439,23 +362,21 @@ internal class DefaultTimeline( val builtSendingEvents = mutableListOf() if (hasReachedEnd(Timeline.Direction.FORWARDS) && !hasMoreInCache(Timeline.Direction.FORWARDS)) { uiEchoManager.getInMemorySendingEvents() - .filterSendingEventsTo(builtSendingEvents) + .updateWithUiEchoInto(builtSendingEvents) sendingEvents .filter { timelineEvent -> builtSendingEvents.none { it.eventId == timelineEvent.eventId } } .map { timelineEventMapper.map(it) } - .filterSendingEventsTo(builtSendingEvents) + .updateWithUiEchoInto(builtSendingEvents) } return builtSendingEvents } - private fun List.filterSendingEventsTo(target: MutableList) { + private fun List.updateWithUiEchoInto(target: MutableList) { target.addAll( - // Filter out sending event that are not displayable! - filterEventsWithSettings(settings) - // Get most up to date send state (in memory) - .map { uiEchoManager.updateSentStateWithUiEcho(it) } + // Get most up to date send state (in memory) + map { uiEchoManager.updateSentStateWithUiEcho(it) } ) } @@ -465,14 +386,14 @@ internal class DefaultTimeline( private fun getState(direction: Timeline.Direction): TimelineState { return when (direction) { - Timeline.Direction.FORWARDS -> forwardsState.get() + Timeline.Direction.FORWARDS -> forwardsState.get() Timeline.Direction.BACKWARDS -> backwardsState.get() } } private fun updateState(direction: Timeline.Direction, update: (TimelineState) -> TimelineState) { val stateReference = when (direction) { - Timeline.Direction.FORWARDS -> forwardsState + Timeline.Direction.FORWARDS -> forwardsState Timeline.Direction.BACKWARDS -> backwardsState } val currentValue = stateReference.get() @@ -487,9 +408,9 @@ internal class DefaultTimeline( var shouldFetchInitialEvent = false val currentInitialEventId = initialEventId val initialDisplayIndex = if (currentInitialEventId == null) { - nonFilteredEvents.firstOrNull()?.displayIndex + timelineEvents.firstOrNull()?.displayIndex } else { - val initialEvent = nonFilteredEvents.where() + val initialEvent = timelineEvents.where() .equalTo(TimelineEventEntityFields.EVENT_ID, initialEventId) .findFirst() @@ -501,7 +422,7 @@ internal class DefaultTimeline( if (currentInitialEventId != null && shouldFetchInitialEvent) { fetchEvent(currentInitialEventId) } else { - val count = filteredEvents.size.coerceAtMost(settings.initialSize) + val count = timelineEvents.size.coerceAtMost(settings.initialSize) if (initialEventId == null) { paginateInternal(initialDisplayIndex, Timeline.Direction.BACKWARDS, count) } else { @@ -541,8 +462,7 @@ internal class DefaultTimeline( val eventEntity = results[index] eventEntity?.eventId?.let { eventId -> postSnapshot = rebuildEvent(eventId) { - val builtEvent = buildTimelineEvent(eventEntity) - listOf(builtEvent).filterEventsWithSettings(settings).firstOrNull() + buildTimelineEvent(eventEntity) } || postSnapshot } } @@ -563,9 +483,9 @@ internal class DefaultTimeline( // We are in the case where event exists, but we do not know the token. // Fetch (again) the last event to get a token val lastKnownEventId = if (direction == Timeline.Direction.FORWARDS) { - nonFilteredEvents.firstOrNull()?.eventId + timelineEvents.firstOrNull()?.eventId } else { - nonFilteredEvents.lastOrNull()?.eventId + timelineEvents.lastOrNull()?.eventId } if (lastKnownEventId == null) { updateState(direction) { it.copy(isPaginating = false, requestedPaginationCount = 0) } @@ -636,7 +556,7 @@ internal class DefaultTimeline( * Return the current Chunk */ private fun getLiveChunk(): ChunkEntity? { - return nonFilteredEvents.firstOrNull()?.chunk?.firstOrNull() + return timelineEvents.firstOrNull()?.chunk?.firstOrNull() } /** @@ -680,14 +600,13 @@ internal class DefaultTimeline( val time = System.currentTimeMillis() - start Timber.v("Built ${offsetResults.size} items from db in $time ms") // For the case where wo reach the lastForward chunk - updateLoadingStates(filteredEvents) + updateLoadingStates(timelineEvents) return offsetResults.size } private fun buildTimelineEvent(eventEntity: TimelineEventEntity) = timelineEventMapper.map( timelineEventEntity = eventEntity, - buildReadReceipts = settings.buildReadReceipts, - correctedReadReceipts = hiddenReadReceipts.correctedReadReceipts(eventEntity.eventId) + buildReadReceipts = settings.buildReadReceipts ).let { // eventually enhance with ui echo? (uiEchoManager.decorateEventWithReactionUiEcho(it) ?: it) @@ -699,7 +618,7 @@ internal class DefaultTimeline( private fun getOffsetResults(startDisplayIndex: Int, direction: Timeline.Direction, count: Long): RealmResults { - val offsetQuery = filteredEvents.where() + val offsetQuery = timelineEvents.where() if (direction == Timeline.Direction.BACKWARDS) { offsetQuery .sort(TimelineEventEntityFields.DISPLAY_INDEX, Sort.DESCENDING) @@ -747,7 +666,7 @@ internal class DefaultTimeline( if (isReady.get().not()) { return@post } - updateLoadingStates(filteredEvents) + updateLoadingStates(timelineEvents) val snapshot = createSnapshot() val runnable = Runnable { listeners.forEach { @@ -783,10 +702,10 @@ internal class DefaultTimeline( return object : MatrixCallback { override fun onSuccess(data: TokenChunkEventPersistor.Result) { when (data) { - TokenChunkEventPersistor.Result.SUCCESS -> { + TokenChunkEventPersistor.Result.SUCCESS -> { Timber.v("Success fetching $limit items $direction from pagination request") } - TokenChunkEventPersistor.Result.REACHED_END -> { + TokenChunkEventPersistor.Result.REACHED_END -> { postSnapshot() } TokenChunkEventPersistor.Result.SHOULD_FETCH_MORE -> diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt index c3714a1303..8de36d0427 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/DefaultTimelineService.kt @@ -31,7 +31,6 @@ import org.matrix.android.sdk.api.session.room.timeline.TimelineService import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.database.RealmSessionProvider -import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper import org.matrix.android.sdk.internal.database.mapper.TimelineEventMapper import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields @@ -52,7 +51,6 @@ internal class DefaultTimelineService @AssistedInject constructor( private val paginationTask: PaginationTask, private val fetchTokenAndPaginateTask: FetchTokenAndPaginateTask, private val timelineEventMapper: TimelineEventMapper, - private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val loadRoomMembersTask: LoadRoomMembersTask, private val readReceiptHandler: ReadReceiptHandler ) : TimelineService { @@ -72,7 +70,6 @@ internal class DefaultTimelineService @AssistedInject constructor( paginationTask = paginationTask, timelineEventMapper = timelineEventMapper, settings = settings, - hiddenReadReceipts = TimelineHiddenReadReceipts(readReceiptsSummaryMapper, roomId, settings), timelineInput = timelineInput, eventDecryptor = eventDecryptor, fetchTokenAndPaginateTask = fetchTokenAndPaginateTask, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt deleted file mode 100644 index b2c8021f3b..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/Extensions.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2021 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session.room.timeline - -import io.realm.RealmQuery -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.toModel -import org.matrix.android.sdk.api.session.room.model.message.MessageContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.query.filterEvents - -internal fun RealmQuery.filterEventsWithSettings(settings: TimelineSettings): RealmQuery { - return filterEvents(settings.filters) -} - -internal fun List.filterEventsWithSettings(settings: TimelineSettings): List { - return filter { event -> - val filterType = !settings.filters.filterTypes - || settings.filters.allowedTypes.any { it.eventType == event.root.type && (it.stateKey == null || it.stateKey == event.root.senderId) } - if (!filterType) return@filter false - - val filterEdits = if (settings.filters.filterEdits && event.root.getClearType() == EventType.MESSAGE) { - val messageContent = event.root.getClearContent().toModel() - messageContent?.relatesTo?.type != RelationType.REPLACE && messageContent?.relatesTo?.type != RelationType.RESPONSE - } else { - true - } - if (!filterEdits) return@filter false - - val filterRedacted = settings.filters.filterRedacted && event.root.isRedacted() - !filterRedacted - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt deleted file mode 100644 index 0ade8ad3b8..0000000000 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/TimelineHiddenReadReceipts.kt +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright 2020 The Matrix.org Foundation C.I.C. - * - * 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 org.matrix.android.sdk.internal.session.room.timeline - -import android.util.SparseArray -import io.realm.OrderedRealmCollectionChangeListener -import io.realm.Realm -import io.realm.RealmQuery -import io.realm.RealmResults -import org.matrix.android.sdk.api.session.room.model.ReadReceipt -import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings -import org.matrix.android.sdk.internal.database.mapper.ReadReceiptsSummaryMapper -import org.matrix.android.sdk.internal.database.model.EventEntityFields -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntity -import org.matrix.android.sdk.internal.database.model.ReadReceiptsSummaryEntityFields -import org.matrix.android.sdk.internal.database.model.TimelineEventEntity -import org.matrix.android.sdk.internal.database.model.TimelineEventEntityFields -import org.matrix.android.sdk.internal.database.query.TimelineEventFilter -import org.matrix.android.sdk.internal.database.query.whereInRoom - -/** - * This class is responsible for handling the read receipts for hidden events (check [TimelineSettings] to see filtering). - * When an hidden event has read receipts, we want to transfer these read receipts on the first older displayed event. - * It has to be used in [DefaultTimeline] and we should call the [start] and [dispose] methods to properly handle realm subscription. - */ -internal class TimelineHiddenReadReceipts constructor(private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, - private val roomId: String, - private val settings: TimelineSettings) { - - interface Delegate { - fun rebuildEvent(eventId: String, readReceipts: List): Boolean - fun onReadReceiptsUpdated() - } - - private val correctedReadReceiptsEventByIndex = SparseArray() - private val correctedReadReceiptsByEvent = HashMap>() - - private lateinit var hiddenReadReceipts: RealmResults - private lateinit var nonFilteredEvents: RealmResults - private lateinit var filteredEvents: RealmResults - private lateinit var delegate: Delegate - - private val hiddenReadReceiptsListener = OrderedRealmCollectionChangeListener> { collection, changeSet -> - if (!collection.isLoaded || !collection.isValid) { - return@OrderedRealmCollectionChangeListener - } - var hasChange = false - // Deletion here means we don't have any readReceipts for the given hidden events - changeSet.deletions.forEach { - val eventId = correctedReadReceiptsEventByIndex.get(it, "") - val timelineEvent = filteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, eventId) - .findFirst() - - // We are rebuilding the corresponding event with only his own RR - val readReceipts = readReceiptsSummaryMapper.map(timelineEvent?.readReceipts) - hasChange = delegate.rebuildEvent(eventId, readReceipts) || hasChange - } - correctedReadReceiptsEventByIndex.clear() - correctedReadReceiptsByEvent.clear() - for (index in 0 until hiddenReadReceipts.size) { - val summary = hiddenReadReceipts[index] ?: continue - val timelineEvent = summary.timelineEvent?.firstOrNull() ?: continue - val isLoaded = nonFilteredEvents.where() - .equalTo(TimelineEventEntityFields.EVENT_ID, timelineEvent.eventId).findFirst() != null - val displayIndex = timelineEvent.displayIndex - - if (isLoaded) { - // Then we are looking for the first displayable event after the hidden one - val firstDisplayedEvent = filteredEvents.where() - .lessThanOrEqualTo(TimelineEventEntityFields.DISPLAY_INDEX, displayIndex) - .findFirst() - - // If we find one, we should - if (firstDisplayedEvent != null) { - correctedReadReceiptsEventByIndex.put(index, firstDisplayedEvent.eventId) - correctedReadReceiptsByEvent - .getOrPut(firstDisplayedEvent.eventId, { - ArrayList(readReceiptsSummaryMapper.map(firstDisplayedEvent.readReceipts)) - }) - .addAll(readReceiptsSummaryMapper.map(summary)) - } - } - } - if (correctedReadReceiptsByEvent.isNotEmpty()) { - correctedReadReceiptsByEvent.forEach { (eventId, correctedReadReceipts) -> - val sortedReadReceipts = correctedReadReceipts.sortedByDescending { - it.originServerTs - } - hasChange = delegate.rebuildEvent(eventId, sortedReadReceipts) || hasChange - } - } - if (hasChange) { - delegate.onReadReceiptsUpdated() - } - } - - /** - * Start the realm query subscription. Has to be called on an HandlerThread - */ - fun start(realm: Realm, - filteredEvents: RealmResults, - nonFilteredEvents: RealmResults, - delegate: Delegate) { - this.filteredEvents = filteredEvents - this.nonFilteredEvents = nonFilteredEvents - this.delegate = delegate - // We are looking for read receipts set on hidden events. - // We only accept those with a timelineEvent (so coming from pagination/sync). - this.hiddenReadReceipts = ReadReceiptsSummaryEntity.whereInRoom(realm, roomId) - .isNotEmpty(ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.`$`) - .isNotEmpty(ReadReceiptsSummaryEntityFields.READ_RECEIPTS.`$`) - .filterReceiptsWithSettings() - .findAllAsync() - .also { it.addChangeListener(hiddenReadReceiptsListener) } - } - - /** - * Dispose the realm query subscription. Has to be called on an HandlerThread - */ - fun dispose() { - if (this::hiddenReadReceipts.isInitialized) { - this.hiddenReadReceipts.removeAllChangeListeners() - } - } - - /** - * Return the current corrected [ReadReceipt] list for an event, or null - */ - fun correctedReadReceipts(eventId: String?): List? { - return correctedReadReceiptsByEvent[eventId] - } - - /** - * We are looking for receipts related to filtered events. So, it's the opposite of [DefaultTimeline.filterEventsWithSettings] method. - */ - private fun RealmQuery.filterReceiptsWithSettings(): RealmQuery { - beginGroup() - var needOr = false - if (settings.filters.filterTypes) { - beginGroup() - // Events: A, B, C, D, (E and S1), F, G, (H and S1), I - // Allowed: A, B, C, (E and S1), G, (H and S2) - // Result: D, F, H, I - settings.filters.allowedTypes.forEachIndexed { index, filter -> - if (filter.stateKey == null) { - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType) - } else { - beginGroup() - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.TYPE}", filter.eventType) - or() - notEqualTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.STATE_KEY}", filter.stateKey) - endGroup() - } - if (index != settings.filters.allowedTypes.size - 1) { - and() - } - } - endGroup() - needOr = true - } - if (settings.filters.filterUseless) { - if (needOr) or() - equalTo("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.IS_USELESS}", true) - needOr = true - } - if (settings.filters.filterEdits) { - if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.EDIT) - or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.CONTENT}", TimelineEventFilter.Content.RESPONSE) - needOr = true - } - if (settings.filters.filterRedacted) { - if (needOr) or() - like("${ReadReceiptsSummaryEntityFields.TIMELINE_EVENT.ROOT}.${EventEntityFields.UNSIGNED_DATA}", TimelineEventFilter.Unsigned.REDACTED) - } - endGroup() - return this - } -} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt index 67d0d90d77..4804fbd731 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/timeline/UIEchoManager.kt @@ -70,15 +70,12 @@ internal class UIEchoManager( return existingState != sendState } - // return true if should update - fun onLocalEchoCreated(timelineEvent: TimelineEvent): Boolean { - var postSnapshot = false - + fun onLocalEchoCreated(timelineEvent: TimelineEvent) { // Manage some ui echos (do it before filter because actual event could be filtered out) when (timelineEvent.root.getClearType()) { EventType.REDACTION -> { } - EventType.REACTION -> { + EventType.REACTION -> { val content = timelineEvent.root.content?.toModel() if (RelationType.ANNOTATION == content?.relatesTo?.type) { val reaction = content.relatesTo.key @@ -91,21 +88,14 @@ internal class UIEchoManager( reaction = reaction ) ) - postSnapshot = listener.rebuildEvent(relatedEventID) { + listener.rebuildEvent(relatedEventID) { decorateEventWithReactionUiEcho(it) - } || postSnapshot + } } } } - - // do not add events that would have been filtered - if (listOf(timelineEvent).filterEventsWithSettings(settings).isNotEmpty()) { - Timber.v("On local echo created: ${timelineEvent.eventId}") - inMemorySendingEvents.add(0, timelineEvent) - postSnapshot = true - } - - return postSnapshot + Timber.v("On local echo created: ${timelineEvent.eventId}") + inMemorySendingEvents.add(0, timelineEvent) } fun decorateEventWithReactionUiEcho(timelineEvent: TimelineEvent): TimelineEvent? { diff --git a/vector/build.gradle b/vector/build.gradle index e3a7b090e3..b7ec0f76f4 100644 --- a/vector/build.gradle +++ b/vector/build.gradle @@ -339,7 +339,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' // Debug - implementation 'com.facebook.stetho:stetho:1.5.1' + implementation 'com.facebook.stetho:stetho:1.6.0' // Phone number https://github.com/google/libphonenumber implementation 'com.googlecode.libphonenumber:libphonenumber:8.12.20' 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..c51573bf21 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,17 @@ import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents abstract class TimelineEmptyItem : VectorEpoxyModel(), ItemWithEvents { @EpoxyAttribute lateinit var eventId: String + @EpoxyAttribute var notBlank: Boolean = false + + override fun isVisible() = false + + override fun bind(holder: Holder) { + super.bind(holder) + holder.view.updateLayoutParams { + // Force height to 1px so scrolling works correctly + this.height = if (notBlank) 1 else 0 + } + } override fun getEventIds(): List { return listOf(eventId) diff --git a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt index f7d7b3864e..9ab3b9bf45 100644 --- a/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt +++ b/vector/src/main/java/im/vector/app/core/resources/UserPreferencesProvider.kt @@ -41,7 +41,11 @@ class UserPreferencesProvider @Inject constructor(private val vectorPreferences: vectorPreferences.neverShowLongClickOnRoomHelpAgain() } - fun shouldShowRoomMemberStateEvents(): Boolean { - return vectorPreferences.showRoomMemberStateEvents() + fun shouldShowJoinLeaves(): Boolean { + return vectorPreferences.showJoinLeaveMessages() + } + + fun shouldShowAvatarDisplayNameChanges(): Boolean { + return vectorPreferences.showAvatarDisplayNameChangeMessages() } } 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/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index f116c2b9af..b2e7004d0f 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1205,7 +1205,6 @@ class RoomDetailFragment @Inject constructor( if (summary?.membership == Membership.JOIN) { views.jumpToBottomView.count = summary.notificationCount views.jumpToBottomView.drawBadge = summary.hasUnreadMessages - scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) views.inviteView.visibility = View.GONE if (state.tombstoneEvent == null) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index a26d51d593..6152562850 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -1171,16 +1171,15 @@ class RoomDetailViewModel @AssistedInject constructor( private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) { stopTrackingUnreadMessages() val targetEventId: String = action.eventId - val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId - val indexOfEvent = timeline.getIndexOfEvent(correctedEventId) + val indexOfEvent = timeline.getIndexOfEvent(targetEventId) if (indexOfEvent == null) { // Event is not already in RAM timeline.restartWithEventId(targetEventId) } if (action.highlight) { - setState { copy(highlightedEventId = correctedEventId) } + setState { copy(highlightedEventId = targetEventId) } } - _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(correctedEventId)) + _viewEvents.post(RoomDetailViewEvents.NavigateToEvent(targetEventId)) } private fun handleResendEvent(action: RoomDetailAction.ResendMessage) { @@ -1411,15 +1410,12 @@ class RoomDetailViewModel @AssistedInject constructor( private fun computeUnreadState(events: List, roomSummary: RoomSummary): UnreadState { if (events.isEmpty()) return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown - val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) - val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) - if (firstDisplayableEventId == null || firstDisplayableEventIndex == null) { - return if (timeline.isLive) { - UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) - } else { - UnreadState.Unknown - } - } + val firstDisplayableEventIndex = timeline.getIndexOfEvent(readMarkerIdSnapshot) + ?: return if (timeline.isLive) { + UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot) + } else { + UnreadState.Unknown + } for (i in (firstDisplayableEventIndex - 1) downTo 0) { val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt index 0eb02f5c75..5d3a91f18d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/ScrollOnHighlightedEventCallback.kt @@ -20,7 +20,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import im.vector.app.core.platform.DefaultListUpdateCallback import im.vector.app.features.home.room.detail.timeline.TimelineEventController -import org.matrix.android.sdk.api.session.room.timeline.Timeline import timber.log.Timber import java.util.concurrent.atomic.AtomicReference @@ -33,8 +32,6 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, private val scheduledEventId = AtomicReference() - var timeline: Timeline? = null - override fun onInserted(position: Int, count: Int) { scrollIfNeeded() } @@ -45,9 +42,7 @@ class ScrollOnHighlightedEventCallback(private val recyclerView: RecyclerView, private fun scrollIfNeeded() { val eventId = scheduledEventId.get() ?: return - val nonNullTimeline = timeline ?: return - val correctedEventId = nonNullTimeline.getFirstDisplayableEventId(eventId) - val positionToScroll = timelineEventController.searchPositionOfEvent(correctedEventId) + val positionToScroll = timelineEventController.searchPositionOfEvent(eventId) if (positionToScroll != null) { val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition() val lastVisibleItem = layoutManager.findLastCompletelyVisibleItemPosition() 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..b67527c24c 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,17 +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.features.call.webrtc.WebRtcCallManager 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.factory.TimelineItemFactoryParams import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem @@ -49,6 +53,7 @@ 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.SendStateDecoration import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever import im.vector.app.features.media.ImageContentRenderer @@ -58,6 +63,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 +71,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 +81,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 +154,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 +204,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private val interceptorHelper = TimelineControllerInterceptorHelper( ::positionOfReadMarker, adapterPositionMapping, - vectorPreferences, + userPreferencesProvider, callManager ) @@ -311,7 +317,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec } else { cacheItemData.eventModel } - listOf(eventModel, + listOf( + cacheItemData?.readReceiptsItem?.takeUnless { mergedHeaderItemFactory.isCollapsed(cacheItemData.localId) }, + eventModel, cacheItemData?.mergedHeaderModel, cacheItemData?.formattedDayModel?.takeIf { eventModel != null || cacheItemData.mergedHeaderModel != null } ) @@ -323,61 +331,128 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec private fun buildCacheItemsIfNeeded() = synchronized(modelCache) { hasUTD = false hasReachedInvite = false - if (modelCache.isEmpty()) { return } + val receiptsByEvents = getReadReceiptsByShownEvent() + val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents) (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) + val params = TimelineItemFactoryParams( + event = event, + prevEvent = prevEvent, + nextEvent = nextEvent, + highlightedEventId = eventIdToHighlight, + lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts, + callback = callback + ) + // Should be build if not cached or if model should be refreshed + if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) { + modelCache[position] = buildCacheItem(params) } + 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(params: TimelineItemFactoryParams): CacheItemData { + val event = params.event 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 { + updateUTDStates(event, params.nextEvent) + val eventModel = timelineItemFactory.create(params).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 searchLastSentEventWithoutReadReceipts(receiptsByEvent: Map>): String? { + if (timeline?.isLive == false) { + // If timeline is not live we don't want to show SentStatus + return null + } + for (event in currentSnapshot) { + // If there is any RR on the event, we stop searching for Sent event + if (receiptsByEvent[event.eventId]?.isNotEmpty() == true) { + return null + } + // If the event is not shown, we go to the next one + if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) { + continue + } + // If the event is sent by us, we update the holder with the eventId and stop the search + if (event.root.senderId == session.myUserId && event.root.sendState.isSent()) { + return event.eventId + } + } + return null + } + + private fun getReadReceiptsByShownEvent(): Map> { + val receiptsByEvent = HashMap>() + if (!userPreferencesProvider.shouldShowReadReceipts()) { + return receiptsByEvent + } + var lastShownEventId: String? = null + val itr = currentSnapshot.listIterator(currentSnapshot.size) + while (itr.hasPrevious()) { + val event = itr.previous() + val currentReadReceipts = ArrayList(event.readReceipts).filter { + it.user.userId != session.myUserId + } + 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 +484,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 +514,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/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt index 548f7a3b1c..3df9898078 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt @@ -46,13 +46,11 @@ class CallItemFactory @Inject constructor( private val callManager: WebRtcCallManager ) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event if (event.root.eventId == null) return null val roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, null, null) + val informationData = messageInformationDataFactory.create(params) val callSignalingContent = event.getCallSignallingContent() ?: return null val callId = callSignalingContent.callId ?: return null val call = callManager.getCallById(callId) @@ -68,8 +66,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = call != null ) @@ -80,8 +78,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.INVITED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = call != null ) @@ -92,8 +90,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.REJECTED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = false ) @@ -104,8 +102,8 @@ class CallItemFactory @Inject constructor( callId = callId, callStatus = CallTileTimelineItem.CallStatus.ENDED, callKind = callKind, - callback = callback, - highlight = highlight, + callback = params.callback, + highlight = params.isHighlighted, informationData = informationData, isStillActive = 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..db7b84ed06 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 @@ -25,7 +25,6 @@ import im.vector.app.features.home.room.detail.timeline.helper.MessageInformatio import im.vector.app.features.home.room.detail.timeline.item.DefaultItem import im.vector.app.features.home.room.detail.timeline.item.DefaultItem_ import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: AvatarSizeProvider, @@ -43,8 +42,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) @@ -52,16 +50,14 @@ class DefaultItemFactory @Inject constructor(private val avatarSizeProvider: Ava .attributes(attributes) } - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?, - throwable: Throwable? = null): DefaultItem { + fun create(params: TimelineItemFactoryParams, throwable: Throwable? = null): DefaultItem { + val event = params.event val text = if (throwable == null) { stringProvider.getString(R.string.rendering_event_error_type_of_event_not_handled, event.root.getClearType()) } else { stringProvider.getString(R.string.rendering_event_error_exception, event.root.eventId) } - val informationData = informationDataFactory.create(event, null, null) - return create(text, informationData, highlight, callback) + val informationData = informationDataFactory.create(params) + return create(text, informationData, params.isHighlighted, params.callback) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt index b531e08359..82d3dea311 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptedItemFactory.kt @@ -21,7 +21,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.StringProvider -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.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -33,7 +32,6 @@ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.crypto.MXCryptoError 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.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent import javax.inject.Inject @@ -46,11 +44,8 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat private val attributesFactory: MessageItemAttributesFactory, private val vectorPreferences: VectorPreferences) { - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - highlight: Boolean, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event event.root.eventId ?: return null return when { @@ -109,14 +104,14 @@ class EncryptedItemFactory @Inject constructor(private val messageInformationDat } } - val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) - val attributes = attributesFactory.create(event.root.content.toModel(), informationData, callback) + val informationData = messageInformationDataFactory.create(params) + val attributes = attributesFactory.create(event.root.content.toModel(), informationData, params.callback) return MessageTextItem_() .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlight) + .highlighted(params.isHighlighted) .attributes(attributes) .message(spannableStr) - .movementMethod(createLinkMovementMethod(callback)) + .movementMethod(createLinkMovementMethod(params.callback)) } else -> null } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt index 68716a3eba..1d30136f27 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/EncryptionItemFactory.kt @@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.R import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider -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.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -28,7 +27,6 @@ import im.vector.app.features.home.room.detail.timeline.item.StatusTileTimelineI import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.internal.crypto.MXCRYPTO_ALGORITHM_MEGOLM import org.matrix.android.sdk.internal.crypto.model.event.EncryptionEventContent import javax.inject.Inject @@ -41,15 +39,14 @@ class EncryptionItemFactory @Inject constructor( private val avatarSizeProvider: AvatarSizeProvider, private val session: Session) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): StatusTileTimelineItem? { + fun create(params: TimelineItemFactoryParams): StatusTileTimelineItem? { + val event = params.event if (!event.root.isStateEvent()) { return null } val algorithm = event.root.getClearContent().toModel()?.algorithm - val informationData = informationDataFactory.create(event, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val informationData = informationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val isSafeAlgorithm = algorithm == MXCRYPTO_ALGORITHM_MEGOLM val title: String @@ -86,7 +83,7 @@ class EncryptionItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .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..cb2a067540 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,12 +86,11 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde eventIdToHighlight: String?, requestModelBuild: () -> Unit, callback: TimelineEventController.Callback?): MergedMembershipEventsItem_? { - val prevSameTypeEvents = items.prevSameTypeEvents(currentPosition, 2) - return if (prevSameTypeEvents.isEmpty()) { + val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(items, currentPosition, 2, eventIdToHighlight) + return if (mergedEvents.isEmpty()) { null } else { var highlighted = false - val mergedEvents = (prevSameTypeEvents + listOf(event)).asReversed() val mergedData = ArrayList(mergedEvents.size) mergedEvents.forEach { mergedEvent -> if (!highlighted && mergedEvent.root.eventId == eventIdToHighlight) { @@ -126,8 +126,7 @@ class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolde onCollapsedStateChanged = { mergeItemCollapseStates[event.localId] = it requestModelBuild() - }, - readReceiptsCallback = callback + } ) MergedMembershipEventsItem_() .id(mergeId) @@ -205,7 +204,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/MessageItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt index e969998613..0f214ffb13 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MessageItemFactory.kt @@ -85,7 +85,6 @@ import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_BUTTONS import org.matrix.android.sdk.api.session.room.model.message.OPTION_TYPE_POLL import org.matrix.android.sdk.api.session.room.model.message.getFileName import org.matrix.android.sdk.api.session.room.model.message.getFileUrl -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.getLastMessageContent import org.matrix.android.sdk.api.util.MimeTypes import org.matrix.android.sdk.internal.crypto.attachments.toElementToDecrypt @@ -118,15 +117,13 @@ class MessageItemFactory @Inject constructor( pillsPostProcessorFactory.create(roomId) } - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event + val highlight = params.isHighlighted + val callback = params.callback event.root.eventId ?: return null roomId = event.roomId - val informationData = messageInformationDataFactory.create(event, prevEvent, nextEvent) + val informationData = messageInformationDataFactory.create(params) if (event.root.isRedacted()) { // message is redacted val attributes = messageItemAttributesFactory.create(null, informationData, callback) @@ -142,7 +139,7 @@ class MessageItemFactory @Inject constructor( || event.isEncrypted() && event.root.content.toModel()?.relatesTo?.type == RelationType.REPLACE ) { // This is an edit event, we should display it when debugging as a notice event - return noticeItemFactory.create(event, highlight, callback) + return noticeItemFactory.create(params) } val attributes = messageItemAttributesFactory.create(messageContent, informationData, callback) @@ -158,7 +155,7 @@ class MessageItemFactory @Inject constructor( is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, attributes) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageOptionsContent -> buildOptionsMessageItem(messageContent, informationData, highlight, callback, attributes) - is MessagePollResponseContent -> noticeItemFactory.create(event, highlight, callback) + is MessagePollResponseContent -> noticeItemFactory.create(params) else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback, attributes) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt index dfabf96199..e757b6b47b 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt @@ -17,13 +17,11 @@ package im.vector.app.features.home.room.detail.timeline.factory 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.format.NoticeEventFormatter import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.item.NoticeItem import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_ -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter, @@ -31,24 +29,23 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv private val informationDataFactory: MessageInformationDataFactory, private val avatarSizeProvider: AvatarSizeProvider) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): NoticeItem? { + fun create(params: TimelineItemFactoryParams): NoticeItem? { + val event = params.event val formattedText = eventFormatter.format(event) ?: return null - val informationData = informationDataFactory.create(event, null, null) + val informationData = informationDataFactory.create(params) val attributes = NoticeItem.Attributes( avatarRenderer = avatarRenderer, informationData = informationData, noticeText = formattedText, itemLongClickListener = { view -> - callback?.onEventLongClicked(informationData, null, view) ?: false + params.callback?.onEventLongClicked(informationData, null, view) ?: false }, - readReceiptsCallback = callback, - avatarClickListener = { callback?.onAvatarClicked(informationData) } + readReceiptsCallback = params.callback, + avatarClickListener = { params.callback?.onAvatarClicked(informationData) } ) return NoticeItem_() .leftGuideline(avatarSizeProvider.leftGuideline) - .highlighted(highlight) + .highlighted(params.isHighlighted) .attributes(attributes) } } 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..1d015d1bca --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/ReadReceiptsItemFactory.kt @@ -0,0 +1,49 @@ +/* + * 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.room.model.ReadReceipt +import javax.inject.Inject + +class ReadReceiptsItemFactory @Inject constructor(private val avatarRenderer: AvatarRenderer) { + + fun create(eventId: String, readReceipts: List, callback: TimelineEventController.Callback?): ReadReceiptsItem? { + if (readReceipts.isEmpty()) { + return null + } + val readReceiptsData = readReceipts + .map { + ReadReceiptData(it.user.userId, it.user.avatarUrl, it.user.displayName, it.originServerTs) + } + .toList() + + 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/RoomCreateItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt index 31adbdb8a6..382962f98d 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/RoomCreateItemFactory.kt @@ -20,13 +20,11 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider 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.item.RoomCreateItem_ import me.gujun.android.span.span import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.create.RoomCreateContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject class RoomCreateItemFactory @Inject constructor(private val stringProvider: StringProvider, @@ -34,25 +32,26 @@ class RoomCreateItemFactory @Inject constructor(private val stringProvider: Stri private val session: Session, private val noticeItemFactory: NoticeItemFactory) { - fun create(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event val createRoomContent = event.root.getClearContent().toModel() ?: return null - val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(event, callback) + val predecessorId = createRoomContent.predecessor?.roomId ?: return defaultRendering(params) val roomLink = session.permalinkService().createRoomPermalink(predecessorId) ?: return null val text = span { +stringProvider.getString(R.string.room_tombstone_continuation_description) +"\n" span(stringProvider.getString(R.string.room_tombstone_predecessor_link)) { textDecorationLine = "underline" - onClick = { callback?.onRoomCreateLinkClicked(roomLink) } + onClick = { params.callback?.onRoomCreateLinkClicked(roomLink) } } } return RoomCreateItem_() .text(text) } - private fun defaultRendering(event: TimelineEvent, callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + private fun defaultRendering(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { return if (userPreferencesProvider.shouldShowHiddenEvents()) { - noticeItemFactory.create(event, false, callback) + noticeItemFactory.create(params) } else { null } 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..47bc60eb75 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 @@ -19,8 +19,7 @@ package im.vector.app.features.home.room.detail.timeline.factory import im.vector.app.core.epoxy.TimelineEmptyItem 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,23 +34,21 @@ 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. */ - fun create(event: TimelineEvent, - prevEvent: TimelineEvent?, - nextEvent: TimelineEvent?, - eventIdToHighlight: String?, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*> { - val highlight = event.root.eventId == eventIdToHighlight - + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*> { + val event = params.event val computedModel = try { + if (!timelineEventVisibilityHelper.shouldShowEvent(event, params.highlightedEventId)) { + return buildEmptyItem(event, params.prevEvent, params.highlightedEventId) + } when (event.root.getClearType()) { + // Message itemsX EventType.STICKER, - EventType.MESSAGE -> messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) - // State and call + EventType.MESSAGE -> messageItemFactory.create(params) EventType.STATE_ROOM_TOMBSTONE, EventType.STATE_ROOM_NAME, EventType.STATE_ROOM_TOPIC, @@ -63,68 +60,61 @@ 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.STATE_ROOM_WIDGET_LEGACY, - EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(event, highlight, callback) - EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(event, highlight, callback) - // State room create - EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(event, callback) - // Calls - EventType.CALL_INVITE, - EventType.CALL_HANGUP, - EventType.CALL_REJECT, - EventType.CALL_ANSWER -> callItemFactory.create(event, highlight, callback) - // Crypto - EventType.ENCRYPTED -> { - if (event.root.isRedacted()) { - // Redacted event, let the MessageItemFactory handle it - messageItemFactory.create(event, prevEvent, nextEvent, highlight, callback) - } else { - encryptedItemFactory.create(event, prevEvent, nextEvent, 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.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) + EventType.CALL_NEGOTIATE, + EventType.REACTION, + EventType.STATE_ROOM_POWER_LEVELS -> noticeItemFactory.create(params) + EventType.STATE_ROOM_WIDGET_LEGACY, + EventType.STATE_ROOM_WIDGET -> widgetItemFactory.create(params) + EventType.STATE_ROOM_ENCRYPTION -> encryptionItemFactory.create(params) + // State room create + EventType.STATE_ROOM_CREATE -> roomCreateItemFactory.create(params) + // Calls + EventType.CALL_INVITE, + EventType.CALL_HANGUP, + EventType.CALL_REJECT, + EventType.CALL_ANSWER -> callItemFactory.create(params) + // Crypto + EventType.ENCRYPTED -> { + if (event.root.isRedacted()) { + // Redacted event, let the MessageItemFactory handle it + messageItemFactory.create(params) } else { - null + encryptedItemFactory.create(params) } } EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_DONE -> { - verificationConclusionItemFactory.create(event, highlight, callback) + verificationConclusionItemFactory.create(params) } - // Unhandled event types else -> { // Should only happen when shouldShowHiddenEvents() settings is ON Timber.v("Type ${event.root.getClearType()} not handled") - defaultItemFactory.create(event, highlight, callback) + defaultItemFactory.create(params) } } } catch (throwable: Throwable) { Timber.e(throwable, "failed to create message item") - defaultItemFactory.create(event, highlight, callback, throwable) + defaultItemFactory.create(params, throwable) } - return computedModel ?: buildEmptyItem(event) + return computedModel ?: buildEmptyItem(event, params.prevEvent, params.highlightedEventId) } - private fun buildEmptyItem(timelineEvent: TimelineEvent): TimelineEmptyItem { + private fun buildEmptyItem(timelineEvent: TimelineEvent, prevEvent: TimelineEvent?, highlightedEventId: String?): TimelineEmptyItem { + val isNotBlank = prevEvent == null || timelineEventVisibilityHelper.shouldShowEvent(prevEvent, highlightedEventId) return TimelineEmptyItem_() .id(timelineEvent.localId) .eventId(timelineEvent.eventId) + .notBlank(isNotBlank) } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt new file mode 100644 index 0000000000..f92cd2800a --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt @@ -0,0 +1,31 @@ +/* + * 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.features.home.room.detail.timeline.TimelineEventController +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +data class TimelineItemFactoryParams( + val event: TimelineEvent, + val prevEvent: TimelineEvent? = null, + val nextEvent: TimelineEvent? = null, + val highlightedEventId: String? = null, + val lastSentEventIdWithoutReadReceipts: String? = null, + val callback: TimelineEventController.Callback? = null +) { + val isHighlighted = highlightedEventId == event.eventId +} diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt index 960487140d..e972ddcab5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/VerificationItemFactory.kt @@ -20,7 +20,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.UserPreferencesProvider import im.vector.app.features.home.room.detail.timeline.MessageColorProvider -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.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -35,7 +34,6 @@ import org.matrix.android.sdk.api.session.events.model.RelationType import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.room.model.message.MessageRelationContent import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationCancelContent -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import javax.inject.Inject /** @@ -54,37 +52,35 @@ class VerificationItemFactory @Inject constructor( private val session: Session ) { - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event if (event.root.eventId == null) return null val relContent: MessageRelationContent = event.root.content.toModel() ?: event.root.getClearContent().toModel() - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) - if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback) + if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(params) val refEventId = relContent.relatesTo?.eventId - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) // If we cannot find the referenced request we do not display the done event val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId) - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) // If it's not a request ignore this event // if (refEvent.root.getClearContent().toModel() == null) return ignoredConclusion(event, highlight, callback) - val referenceInformationData = messageInformationDataFactory.create(refEvent, null, null) + val referenceInformationData = messageInformationDataFactory.create(TimelineItemFactoryParams(refEvent)) - val informationData = messageInformationDataFactory.create(event, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val informationData = messageInformationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) when (event.root.getClearType()) { EventType.KEY_VERIFICATION_CANCEL -> { // Is the request referenced is actually really cancelled? val cancelContent = event.root.getClearContent().toModel() - ?: return ignoredConclusion(event, highlight, callback) + ?: return ignoredConclusion(params) when (safeValueOf(cancelContent.code)) { CancelCode.MismatchedCommitment, @@ -107,22 +103,22 @@ class VerificationItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } - else -> return ignoredConclusion(event, highlight, callback) + else -> return ignoredConclusion(params) } } EventType.KEY_VERIFICATION_DONE -> { // Is the request referenced is actually really completed? if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE) { - return ignoredConclusion(event, highlight, callback) + return ignoredConclusion(params) } // We only tale the one sent by me if (informationData.sentByMe) { // We only display the done sent by the other user, the done send by me is ignored - return ignoredConclusion(event, highlight, callback) + return ignoredConclusion(params) } return StatusTileTimelineItem_() .attributes( @@ -140,18 +136,15 @@ class VerificationItemFactory @Inject constructor( readReceiptsCallback = attributes.readReceiptsCallback ) ) - .highlighted(highlight) + .highlighted(params.isHighlighted) .leftGuideline(avatarSizeProvider.leftGuideline) } } return null } - private fun ignoredConclusion(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback? - ): VectorEpoxyModel<*>? { - if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback) + private fun ignoredConclusion(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(params) return null } } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt index a6a88a3444..1fc57489a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt @@ -20,7 +20,6 @@ import im.vector.app.ActiveSessionDataSource import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.resources.StringProvider -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.MessageInformationDataFactory import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory @@ -29,7 +28,6 @@ import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineI import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.widgets.model.WidgetContent import org.matrix.android.sdk.api.session.widgets.model.WidgetType import javax.inject.Inject @@ -47,25 +45,24 @@ class WidgetItemFactory @Inject constructor( private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId - fun create(event: TimelineEvent, - highlight: Boolean, - callback: TimelineEventController.Callback?): VectorEpoxyModel<*>? { + fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? { + val event = params.event val widgetContent: WidgetContent = event.root.getClearContent().toModel() ?: return null val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel() return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) { - WidgetType.Jitsi -> createJitsiItem(event, callback, widgetContent, previousWidgetContent) + WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent) // There is lot of other widget types we could improve here - else -> noticeItemFactory.create(event, highlight, callback) + else -> noticeItemFactory.create(params) } } - private fun createJitsiItem(timelineEvent: TimelineEvent, - callback: TimelineEventController.Callback?, + private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent, previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> { - val informationData = informationDataFactory.create(timelineEvent, null, null) - val attributes = messageItemAttributesFactory.create(null, informationData, callback) + val timelineEvent = params.event + val informationData = informationDataFactory.create(params) + val attributes = messageItemAttributesFactory.create(null, informationData, params.callback) val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName val message = if (widgetContent.isActive()) { 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..14dd311265 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 @@ -19,11 +19,11 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.date.DateFormatKind import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.extensions.localDateTime +import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.PollResponseData import im.vector.app.features.home.room.detail.timeline.item.ReactionInfoData -import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReferencesInfoData import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration import im.vector.app.features.settings.VectorPreferences @@ -51,9 +51,10 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses private val dateFormatter: VectorDateFormatter, private val vectorPreferences: VectorPreferences) { - fun create(event: TimelineEvent, prevEvent: TimelineEvent?, nextEvent: TimelineEvent?): MessageInformationData { - // Non nullability has been tested before - val eventId = event.root.eventId!! + fun create(params: TimelineItemFactoryParams): MessageInformationData { + val event = params.event + val nextEvent = params.nextEvent + val eventId = event.eventId val date = event.root.localDateTime() val nextDate = nextEvent?.root?.localDateTime() @@ -76,9 +77,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses val isSentByMe = event.root.senderId == session.myUserId val sendStateDecoration = if (isSentByMe) { getSendStateDecoration( - eventSendState = event.root.sendState, - prevEventSendState = prevEvent?.root?.sendState, - anyReadReceipts = event.readReceipts.any { it.user.userId != session.myUserId }, + event = event, + lastSentEventWithoutReadReceipts = params.lastSentEventIdWithoutReadReceipts, isMedia = event.root.isAttachmentMessage() ) } else { @@ -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 @@ -131,15 +122,15 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses ) } - private fun getSendStateDecoration(eventSendState: SendState, - prevEventSendState: SendState?, - anyReadReceipts: Boolean, + private fun getSendStateDecoration(event: TimelineEvent, + lastSentEventWithoutReadReceipts: String?, isMedia: Boolean): SendStateDecoration { + val eventSendState = event.root.sendState return if (eventSendState.isSending()) { if (isMedia) SendStateDecoration.SENDING_MEDIA else SendStateDecoration.SENDING_NON_MEDIA } else if (eventSendState.hasFailed()) { SendStateDecoration.FAILED - } else if (eventSendState.isSent() && !prevEventSendState?.isSent().orFalse() && !anyReadReceipts) { + } else if (lastSentEventWithoutReadReceipts == event.eventId) { SendStateDecoration.SENT } else { SendStateDecoration.NONE 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..72fdef9f7d 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 @@ -20,13 +20,14 @@ 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.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 import org.matrix.android.sdk.api.session.room.timeline.Timeline import kotlin.reflect.KMutableProperty0 @@ -34,7 +35,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 +57,39 @@ 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 ItemWithEvents) { + if (epoxyModel.isVisible()) { + atLeastOneVisibleItemSinceLastDaySeparator = true + atLeastOneVisibleItemsBeforeReadMarker = true + } epoxyModel.getEventIds().forEach { eventId -> adapterPositionMapping[eventId] = index - if (eventId == firstUnreadEventId) { + if (epoxyModel.canAppendReadMarker() && 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 +111,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) + .notBlank(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..053b804a82 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 @@ -16,12 +16,14 @@ package im.vector.app.features.home.room.detail.timeline.helper -import im.vector.app.core.extensions.localDateTime import org.matrix.android.sdk.api.session.events.model.EventType 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, @@ -68,7 +70,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 +78,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..580d7d18cf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventVisibilityHelper.kt @@ -0,0 +1,152 @@ +/* + * 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) { + + /** + * @param timelineEvents the events to search in + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * + * @return a list of timeline events which have sequentially the same type following the next direction. + */ + fun nextSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + if (index >= timelineEvents.size - 1) { + return emptyList() + } + val timelineEvent = timelineEvents[index] + val nextSubList = timelineEvents.subList(index, 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, eventIdToHighlight) } + if (filteredSameTypeEvents.size < minSize) { + return emptyList() + } + return filteredSameTypeEvents + } + + /** + * @param timelineEvents the events to search in + * @param index the index to start computing (inclusive) + * @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list + * @param eventIdToHighlight used to compute visibility + * + * @return a list of timeline events which have sequentially the same type following the prev direction. + */ + fun prevSameTypeEvents(timelineEvents: List, index: Int, minSize: Int, eventIdToHighlight: String?): List { + val prevSub = timelineEvents.subList(0, index + 1) + return prevSub + .reversed() + .let { + nextSameTypeEvents(it, 0, minSize, eventIdToHighlight) + } + } + + /** + * @param timelineEvent the event to check for visibility + * @param highlightedEventId can be checked to force visibility to true + * @return true if the event should be shown in the timeline. + */ + fun shouldShowEvent(timelineEvent: TimelineEvent, highlightedEventId: String?): 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 == highlightedEventId) { + 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.shouldShowJoinLeaves()) return true + if ((diff.isAvatarChange || diff.isDisplaynameChange) && !userPreferencesProvider.shouldShowAvatarDisplayNameChanges()) 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 isProfileChanged = !isMembershipChanged && content?.membership == Membership.JOIN + val isDisplaynameChange = isProfileChanged && content?.displayName != prevContent?.displayName + val isAvatarChange = isProfileChanged && 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/helper/TimelineSettingsFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt index 01c7ad3986..3aee65bf19 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineSettingsFactory.kt @@ -17,48 +17,14 @@ package im.vector.app.features.home.room.detail.timeline.helper import im.vector.app.core.resources.UserPreferencesProvider -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.room.timeline.EventTypeFilter -import org.matrix.android.sdk.api.session.room.timeline.TimelineEventFilters import org.matrix.android.sdk.api.session.room.timeline.TimelineSettings import javax.inject.Inject -class TimelineSettingsFactory @Inject constructor( - private val userPreferencesProvider: UserPreferencesProvider, - private val session: Session -) { +class TimelineSettingsFactory @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) { fun create(): TimelineSettings { - return if (userPreferencesProvider.shouldShowHiddenEvents()) { - TimelineSettings( - initialSize = 30, - filters = TimelineEventFilters( - filterEdits = false, - filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), - filterUseless = false, - filterTypes = false), - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) - } else { - val allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES.createAllowedEventTypeFilters() - TimelineSettings( - initialSize = 30, - filters = TimelineEventFilters( - filterEdits = true, - filterRedacted = userPreferencesProvider.shouldShowRedactedMessages().not(), - filterUseless = true, - filterTypes = true, - allowedTypes = allowedTypes), - buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) - } - } - - private fun List.createAllowedEventTypeFilters(): List { - return map { - EventTypeFilter( - eventType = it, - stateKey = if (it == EventType.STATE_ROOM_MEMBER && !userPreferencesProvider.shouldShowRoomMemberStateEvents()) session.myUserId else null - ) - } + return TimelineSettings( + initialSize = 30, + buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts()) } } 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..39c04af089 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 @@ -24,7 +24,6 @@ import androidx.annotation.IdRes import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.core.ui.views.ShieldImageView -import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.MessageColorProvider import im.vector.app.features.home.room.detail.timeline.TimelineEventController @@ -41,10 +40,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 +64,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 +100,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..7d539f9df7 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 @@ -26,7 +26,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.platform.CheckableView -import im.vector.app.core.ui.views.ReadReceiptsView import im.vector.app.core.utils.DimensionConverter /** @@ -56,7 +55,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..1c56a0809e 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 @@ -19,10 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.item import android.view.View import android.widget.TextView import androidx.annotation.IdRes -import androidx.core.view.isVisible import im.vector.app.R import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController import org.matrix.android.sdk.api.util.MatrixItem abstract class BasedMergedItem : BaseEventItem() { @@ -41,8 +39,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 +68,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..e6c6e1d372 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 @@ -22,9 +22,7 @@ import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R -import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class DefaultItem : BaseEventItem() { @@ -32,21 +30,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) } @@ -65,8 +57,7 @@ abstract class DefaultItem : BaseEventItem() { val avatarRenderer: AvatarRenderer, val informationData: MessageInformationData, val text: CharSequence, - val itemLongClickListener: View.OnLongClickListener? = null, - val readReceiptsCallback: TimelineEventController.ReadReceiptsCallback? = null + val itemLongClickListener: View.OnLongClickListener? = null ) companion object { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt index cf4211bb2c..050cba0d56 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ItemWithEvents.kt @@ -22,4 +22,8 @@ interface ItemWithEvents { * Will generally get only one, but it handles the merged items. */ fun getEventIds(): List + + fun canAppendReadMarker(): Boolean = true + + fun isVisible(): Boolean = true } 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..a52ddf8336 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 @@ -21,12 +21,10 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.core.view.children -import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import im.vector.app.R import im.vector.app.features.home.AvatarRenderer -import im.vector.app.features.home.room.detail.timeline.TimelineEventController @EpoxyModelClass(layout = R.layout.item_timeline_event_base_noinfo) abstract class MergedMembershipEventsItem : BasedMergedItem() { @@ -56,8 +54,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..4876e8e500 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 @@ -25,7 +25,6 @@ import im.vector.app.R import im.vector.app.core.epoxy.ClickListener import im.vector.app.core.epoxy.onClick import im.vector.app.core.ui.views.ShieldImageView -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 org.matrix.android.sdk.api.crypto.RoomEncryptionTrustLevel @@ -36,16 +35,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 +56,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..b88afb0598 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/ReadReceiptsItem.kt @@ -0,0 +1,53 @@ +/* + * 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 canAppendReadMarker(): Boolean = false + + 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/java/im/vector/app/features/settings/VectorPreferences.kt b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt index a1151162c8..9b043cfc7c 100755 --- a/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt +++ b/vector/src/main/java/im/vector/app/features/settings/VectorPreferences.kt @@ -357,15 +357,6 @@ class VectorPreferences @Inject constructor(private val context: Context) { return defaultPrefs.getBoolean(SETTINGS_12_24_TIMESTAMPS_KEY, false) } - /** - * Tells if all room member state events should be shown in the messages list. - * - * @return true all room member state events should be shown in the messages list. - */ - fun showRoomMemberStateEvents(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_SHOW_ROOM_MEMBER_STATE_EVENTS_KEY, true) - } - /** * Tells if the join and leave membership events should be shown in the messages list. * 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 diff --git a/vector/src/main/res/xml/vector_settings_preferences.xml b/vector/src/main/res/xml/vector_settings_preferences.xml index 6a3c60a021..1d39791ad8 100644 --- a/vector/src/main/res/xml/vector_settings_preferences.xml +++ b/vector/src/main/res/xml/vector_settings_preferences.xml @@ -88,9 +88,15 @@ + android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" + android:summary="@string/settings_show_join_leave_messages_summary" + android:title="@string/settings_show_join_leave_messages" /> + +