Read marker: handle the jump to read marker
This commit is contained in:
parent
d9982076f9
commit
64d73ae8e6
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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() {
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user