From 2db315219acfd08978cd30efb9096d8e3e9e97d9 Mon Sep 17 00:00:00 2001 From: SpiritCroc Date: Mon, 2 Aug 2021 10:51:15 +0200 Subject: [PATCH] Lab setting to load rooms at first unread message Change-Id: I781e5a32d8557939c51387eadf1387cba0d3b149 --- .../sdk/api/session/room/timeline/Timeline.kt | 10 +++++++++ .../session/room/timeline/DefaultTimeline.kt | 14 ++++++++++-- .../home/room/detail/RoomDetailViewModel.kt | 22 ++++++++++++++----- .../room/detail/ScrollOnNewMessageCallback.kt | 7 ++++-- .../features/settings/VectorPreferences.kt | 6 +++++ vector/src/main/res/values/strings_sc.xml | 3 +++ .../src/main/res/xml/vector_settings_labs.xml | 6 +++++ 7 files changed, 58 insertions(+), 10 deletions(-) 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 3b9ba39c1e..ca374d6eae 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 @@ -66,6 +66,16 @@ interface Timeline { */ fun setInitialEventId(eventId: String?) + /** + * Offset for the initial event, e.g. if we want to load the event just below said id + */ + fun getInitialEventIdOffset(): Int + + /** + * Set the offset for the initial event, e.g. if we want to load the event just below said id + */ + fun setInitialEventIdOffset(offset: Int) + /** * Check if the timeline can be enriched by paginating. * @param direction the direction to check in 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 19ba826ad5..c66b60ada5 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 @@ -61,6 +61,7 @@ private const val MIN_FETCHING_COUNT = 30 internal class DefaultTimeline( private val roomId: String, private var initialEventId: String? = null, + private var initialEventIdOffset: Int = 0, private val realmConfiguration: RealmConfiguration, private val taskExecutor: TaskExecutor, private val contextOfEventTask: GetContextOfEventTask, @@ -145,7 +146,7 @@ internal class DefaultTimeline( override fun start() { if (isStarted.compareAndSet(false, true)) { - Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId") + Timber.v("Start timeline for roomId: $roomId and eventId: $initialEventId (offset $initialEventIdOffset)") timelineInput.listeners.add(this) BACKGROUND_HANDLER.post { eventDecryptor.start() @@ -195,7 +196,7 @@ internal class DefaultTimeline( if (isStarted.compareAndSet(true, false)) { isReady.set(false) timelineInput.listeners.remove(this) - Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId") + Timber.v("Dispose timeline for roomId: $roomId and eventId: $initialEventId (offset $initialEventIdOffset)") cancelableBag.cancel() BACKGROUND_HANDLER.removeCallbacksAndMessages(null) BACKGROUND_HANDLER.post { @@ -217,6 +218,7 @@ internal class DefaultTimeline( override fun restartWithEventId(eventId: String?) { dispose() initialEventId = eventId + initialEventIdOffset = 0 start() postSnapshot() } @@ -229,6 +231,14 @@ internal class DefaultTimeline( initialEventId = eventId } + override fun getInitialEventIdOffset(): Int { + return initialEventIdOffset + } + + override fun setInitialEventIdOffset(offset: Int) { + initialEventIdOffset = offset + } + override fun getTimelineEventAtIndex(index: Int): TimelineEvent? { return builtEvents.getOrNull(index) } 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 de223a94b6..280610437e 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 @@ -131,11 +131,16 @@ class RoomDetailViewModel @AssistedInject constructor( Timeline.Listener, ChatEffectManager.Delegate, CallProtocolsChecker.Listener { private val room = session.getRoom(initialState.roomId)!! - private val eventId = initialState.eventId + private val eventId = initialState.eventId ?: if (vectorPreferences.loadRoomAtFirstUnread()) room.roomSummary()?.readMarkerId else null private val invisibleEventsObservable = BehaviorRelay.create() private val visibleEventsObservable = BehaviorRelay.create() private var timelineEvents = PublishRelay.create>() - val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId) + val timeline = timelineFactory.createTimeline(viewModelScope, room, eventId).apply { + // Target the event just below $eventId in case that it is the readMarkerId + if (initialState.eventId == null && eventId != null) { + setInitialEventIdOffset(-1) + } + } // Same lifecycle than the ViewModel (survive to screen rotation) val previewUrlRetriever = PreviewUrlRetriever(session, viewModelScope) @@ -184,8 +189,10 @@ class RoomDetailViewModel @AssistedInject constructor( observeActiveRoomWidgets() observePowerLevel() room.getRoomSummaryLive() - viewModelScope.launch(Dispatchers.IO) { - tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } + if (!vectorPreferences.loadRoomAtFirstUnread()) { + viewModelScope.launch(Dispatchers.IO) { + tryOrNull { room.markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT) } + } } // Inform the SDK that the room is displayed viewModelScope.launch(Dispatchers.IO) { @@ -507,6 +514,9 @@ class RoomDetailViewModel @AssistedInject constructor( mostRecentDisplayedEvent?.root?.eventId?.also { session.coroutineScope.launch(NonCancellable) { tryOrNull { room.setReadMarker(it) } + if (vectorPreferences.loadRoomAtFirstUnread()) { + tryOrNull { room.setReadReceipt(it) } + } } } mostRecentDisplayedEvent = null @@ -1312,10 +1322,10 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun observeEventDisplayedActions() { - // We are buffering scroll events for one second + // We are buffering scroll events for half a second // and keep the most recent one to set the read receipt on. visibleEventsObservable - .buffer(1, TimeUnit.SECONDS) + .buffer(500, TimeUnit.MILLISECONDS) .filter { it.isNotEmpty() } .subscribeBy(onNext = { actions -> val bufferedMostRecentDisplayedEvent = actions.maxByOrNull { it.event.displayIndex }?.event ?: return@subscribeBy 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 eb2fa203b0..6f84854223 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 @@ -23,6 +23,7 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents import org.matrix.android.sdk.api.extensions.tryOrNull import java.util.concurrent.CopyOnWriteArrayList +import kotlin.math.max class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private val timelineEventController: TimelineEventController, @@ -32,7 +33,6 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, private var forceScroll = false var initialForceScroll = false var initialForceScrollEventId: String? = null - get() = field ?: timelineEventController.timeline?.getInitialEventId() fun addNewTimelineEventIds(eventIds: List) { // Disable initial force scroll @@ -57,8 +57,10 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, override fun onInserted(position: Int, count: Int) { if (initialForceScroll) { var scrollToEvent = initialForceScrollEventId + var scrollOffset = 0 if (initialForceScrollEventId == null) { scrollToEvent = timelineEventController.timeline?.getInitialEventId() + scrollOffset = timelineEventController.timeline?.getInitialEventIdOffset() ?: 0 } if (scrollToEvent == null) { layoutManager.scrollToPositionWithOffset(0, 0) @@ -67,7 +69,8 @@ class ScrollOnNewMessageCallback(private val layoutManager: LinearLayoutManager, // Scroll such that the scrolled-to event is moved down 1/3 of the screen. // To do that, we actually scroll the view above out by 2/3 (since we can only control the distance // from the bottom of the view, not the top). - layoutManager.scrollToPositionWithOffset(it + 1, parentView.measuredHeight * 2 / 3) + val scrollToPosition = max(it + scrollOffset + 1, 0) + layoutManager.scrollToPositionWithOffset(scrollToPosition, parentView.measuredHeight * 2 / 3) } } return 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 b8f09d0f98..07253658c8 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 @@ -203,6 +203,7 @@ class VectorPreferences @Inject constructor(private val context: Context) { private const val SETTINGS_USER_COLOR_MODE_DM = "SETTINGS_USER_COLOR_MODE_DM" private const val SETTINGS_USER_COLOR_MODE_DEFAULT = "SETTINGS_USER_COLOR_MODE_DEFAULT" private const val SETTINGS_USER_COLOR_MODE_PUBLIC_ROOM = "SETTINGS_USER_COLOR_MODE_PUBLIC_ROOM" + private const val SETTINGS_OPEN_CHATS_AT_FIRST_UNREAD = "SETTINGS_OPEN_CHATS_AT_FIRST_UNREAD" private const val DID_ASK_TO_ENABLE_SESSION_PUSH = "DID_ASK_TO_ENABLE_SESSION_PUSH" @@ -997,6 +998,11 @@ class VectorPreferences @Inject constructor(private val context: Context) { }, MatrixItemColorProvider.USER_COLORING_DEFAULT) ?: MatrixItemColorProvider.USER_COLORING_DEFAULT } + // SC addition + fun loadRoomAtFirstUnread(): Boolean { + return defaultPrefs.getBoolean(SETTINGS_OPEN_CHATS_AT_FIRST_UNREAD, false) + } + /** * The user enable protecting app access with pin code. * Currently we use the pin code store to know if the pin is enabled, so this is not used diff --git a/vector/src/main/res/values/strings_sc.xml b/vector/src/main/res/values/strings_sc.xml index 26926a90f8..e5de7cee99 100644 --- a/vector/src/main/res/values/strings_sc.xml +++ b/vector/src/main/res/values/strings_sc.xml @@ -84,6 +84,9 @@ Accent color for dark themes Note that these color settings only apply to SC themes, and not Element themes. + Open at first unread + Open chats at the first unread message instead of at the bottom. + Light green Dark green diff --git a/vector/src/main/res/xml/vector_settings_labs.xml b/vector/src/main/res/xml/vector_settings_labs.xml index 91d3293763..acb85022fd 100644 --- a/vector/src/main/res/xml/vector_settings_labs.xml +++ b/vector/src/main/res/xml/vector_settings_labs.xml @@ -34,6 +34,12 @@ + +