Read marker: handle the jump to read marker

This commit is contained in:
ganfra 2019-11-21 14:42:16 +01:00 committed by Benoit Marty
parent d9982076f9
commit 64d73ae8e6
9 changed files with 98 additions and 84 deletions

View File

@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task<GetContextOfEventTask.Params, To
data class Params( data class Params(
val roomId: String, val roomId: String,
val eventId: String val eventId: String,
val limit: Int
) )
} }
@ -38,7 +39,7 @@ internal class DefaultGetContextOfEventTask @Inject constructor(private val room
override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result { override suspend fun execute(params: GetContextOfEventTask.Params): TokenChunkEventPersistor.Result {
val filter = filterRepository.getRoomFilter() val filter = filterRepository.getRoomFilter()
val response = executeRequest<EventContextResponse> { val response = executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, 0, filter) apiCall = roomAPI.getContextOfEvent(params.roomId, params.eventId, params.limit, filter)
} }
return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS) return tokenChunkEventPersistor.insertInDb(response, params.roomId, PaginationDirection.BACKWARDS)
} }

View File

@ -633,7 +633,7 @@ internal class DefaultTimeline(
} }
private fun fetchEvent(eventId: String) { private fun fetchEvent(eventId: String) {
val params = GetContextOfEventTask.Params(roomId, eventId) val params = GetContextOfEventTask.Params(roomId, eventId, settings.initialSize)
cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor) cancelableBag += contextOfEventTask.configureWith(params).executeBy(taskExecutor)
} }

View File

@ -30,6 +30,7 @@ data class EventContextResponse(
@Json(name = "state") override val stateEvents: List<Event> = emptyList() @Json(name = "state") override val stateEvents: List<Event> = emptyList()
) : TokenChunkEvent { ) : TokenChunkEvent {
override val events: List<Event> override val events: List<Event> by lazy {
get() = listOf(event) eventsAfter.reversed() + listOf(event) + eventsBefore
}
} }

View File

@ -474,7 +474,8 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(stateRestorer) it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback) it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback) it.dispatchTo(scrollOnHighlightedEventCallback)
checkJumpToUnreadBanner() updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility()
} }
recyclerView.adapter = timelineEventController.adapter recyclerView.adapter = timelineEventController.adapter
@ -520,19 +521,24 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post { private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
withState(roomDetailViewModel) { withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) { val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown, UnreadState.Unknown,
UnreadState.HasNoUnread -> false UnreadState.HasNoUnread -> false
is UnreadState.ReadMarkerNotLoaded -> true
is UnreadState.HasUnread -> { is UnreadState.HasUnread -> {
if (it.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition() val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker() val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
if (positionOfReadMarker == null) { if (positionOfReadMarker == null) {
it.timeline?.isLive == true && lastVisibleItem > 0 false
} else { } else {
positionOfReadMarker > lastVisibleItem positionOfReadMarker > lastVisibleItem
} }
} else {
false
}
} }
} }
jumpToReadMarkerView.isVisible = showJumpToUnreadBanner jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
@ -1031,14 +1037,10 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onReadMarkerVisible() { override fun onReadMarkerVisible() {
checkJumpToUnreadBanner() updateJumpToReadMarkerViewVisibility()
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState) roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
} }
override fun onReadMarkerInvisible() {
checkJumpToUnreadBanner()
}
// AutocompleteUserPresenter.Callback // AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) { override fun onQueryUsers(query: CharSequence?) {
@ -1244,8 +1246,12 @@ class RoomDetailFragment @Inject constructor(
// JumpToReadMarkerView.Callback // JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) { override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
jumpToReadMarkerView.isVisible = false
if (it.unreadState is UnreadState.HasUnread) { if (it.unreadState is UnreadState.HasUnread) {
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false)) roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.firstUnreadEventId, false))
}
if (it.unreadState is UnreadState.ReadMarkerNotLoaded) {
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.readMarkerId, false))
} }
} }

View File

@ -65,6 +65,7 @@ import im.vector.riotx.features.settings.VectorPreferences
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber import timber.log.Timber
@ -179,6 +180,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun startTrackingUnreadMessages() { private fun startTrackingUnreadMessages() {
trackUnreadMessages.set(true) trackUnreadMessages.set(true)
setState { copy(canShowJumpToReadMarker = false) }
} }
private fun stopTrackingUnreadMessages() { private fun stopTrackingUnreadMessages() {
@ -188,6 +190,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} }
mostRecentDisplayedEvent = null mostRecentDisplayedEvent = null
} }
setState { copy(canShowJumpToReadMarker = true) }
} }
private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) { private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) {
@ -788,29 +791,33 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun getUnreadState() { private fun getUnreadState() {
Observable Observable
.combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>( .combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(
timelineEvents, timelineEvents.observeOn(Schedulers.computation()),
room.rx().liveRoomSummary().unwrap(), room.rx().liveRoomSummary().unwrap(),
BiFunction { timelineEvents, roomSummary -> BiFunction { timelineEvents, roomSummary ->
computeUnreadState(timelineEvents, roomSummary) computeUnreadState(timelineEvents, roomSummary)
} }
) )
.takeUntil { // We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
it != UnreadState.Unknown .distinctUntilChanged { previous, current ->
when {
previous is UnreadState.Unknown || previous is UnreadState.ReadMarkerNotLoaded -> false
current is UnreadState.HasUnread || current is UnreadState.HasNoUnread -> true
else -> false
} }
.subscribe { unreadState ->
setState {
copy(unreadState = unreadState)
} }
.subscribe {
setState { copy(unreadState = it) }
} }
.disposeOnClear() .disposeOnClear()
} }
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState { private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot) val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
?: return UnreadState.Unknown ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId) val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
?: return UnreadState.Unknown ?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
for (i in (firstDisplayableEventIndex - 1) downTo 0) { for (i in (firstDisplayableEventIndex - 1) downTo 0) {
val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
@ -849,7 +856,6 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
override fun onUpdated(snapshot: List<TimelineEvent>) { override fun onUpdated(snapshot: List<TimelineEvent>) {
timelineEvents.accept(snapshot) timelineEvents.accept(snapshot)
setState { copy(currentSnapshot = snapshot) }
} }
override fun onCleared() { override fun onCleared() {

View File

@ -44,7 +44,8 @@ sealed class SendMode(open val text: String) {
sealed class UnreadState { sealed class UnreadState {
object Unknown : UnreadState() object Unknown : UnreadState()
object HasNoUnread : UnreadState() object HasNoUnread : UnreadState()
data class HasUnread(val eventId: String) : UnreadState() data class ReadMarkerNotLoaded(val readMarkerId: String): UnreadState()
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
} }
data class RoomDetailViewState( data class RoomDetailViewState(
@ -59,10 +60,8 @@ data class RoomDetailViewState(
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE, val syncState: SyncState = SyncState.IDLE,
val highlightedEventId: String? = null, val highlightedEventId: String? = null,
val currentSnapshot: List<TimelineEvent> = emptyList(), val unreadState: UnreadState = UnreadState.Unknown,
val hasMoreToLoadForward: Boolean = false, val canShowJumpToReadMarker: Boolean = true
val hasMoreToLoadBackward: Boolean = false,
val unreadState: UnreadState = UnreadState.Unknown
) : MvRxState { ) : MvRxState {
constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId) constructor(args: RoomDetailArgs) : this(roomId = args.roomId, eventId = args.eventId)

View File

@ -83,7 +83,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
interface ReadReceiptsCallback { interface ReadReceiptsCallback {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerVisible() fun onReadMarkerVisible()
fun onReadMarkerInvisible()
} }
interface UrlClickCallback { interface UrlClickCallback {
@ -297,7 +296,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? { private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? {
return when (currentUnreadState) { return when (currentUnreadState) {
is UnreadState.HasUnread -> { is UnreadState.HasUnread -> {
if (event.root.eventId == currentUnreadState.eventId) { if (event.root.eventId == currentUnreadState.firstUnreadEventId) {
TimelineReadMarkerItem_() TimelineReadMarkerItem_()
.also { .also {
it.id("read_marker") it.id("read_marker")
@ -307,8 +306,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
null null
} }
} }
UnreadState.Unknown, else -> null
UnreadState.HasNoUnread -> null
} }
} }
@ -359,6 +357,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val formattedDayModel: DaySeparatorItem? = null, val formattedDayModel: DaySeparatorItem? = null,
val readMarkerModel: TimelineReadMarkerItem? = null val readMarkerModel: TimelineReadMarkerItem? = null
) { ) {
fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId) fun shouldTriggerBuild(unreadState: UnreadState): Boolean {
return mergedHeaderModel != null
|| formattedDayModel != null
|| readMarkerModel != null
|| (unreadState is UnreadState.HasUnread && unreadState.firstUnreadEventId == eventId)
}
} }
} }

View File

@ -27,8 +27,6 @@ class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEve
override fun onVisibilityStateChanged(visibilityState: Int) { override fun onVisibilityStateChanged(visibilityState: Int) {
if (visibilityState == VisibilityState.VISIBLE) { if (visibilityState == VisibilityState.VISIBLE) {
callback?.onReadMarkerVisible() callback?.onReadMarkerVisible()
} else if (visibilityState == VisibilityState.INVISIBLE) {
callback?.onReadMarkerInvisible()
} }
} }
} }