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(
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 {
val filter = filterRepository.getRoomFilter()
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)
}

View File

@ -633,7 +633,7 @@ internal class DefaultTimeline(
}
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)
}

View File

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

View File

@ -474,7 +474,8 @@ class RoomDetailFragment @Inject constructor(
it.dispatchTo(stateRestorer)
it.dispatchTo(scrollOnNewMessageCallback)
it.dispatchTo(scrollOnHighlightedEventCallback)
checkJumpToUnreadBanner()
updateJumpToReadMarkerViewVisibility()
updateJumpToBottomViewVisibility()
}
recyclerView.adapter = timelineEventController.adapter
@ -520,18 +521,23 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun checkJumpToUnreadBanner() = jumpToReadMarkerView.post {
private fun updateJumpToReadMarkerViewVisibility() = jumpToReadMarkerView.post {
withState(roomDetailViewModel) {
val showJumpToUnreadBanner = when (it.unreadState) {
UnreadState.Unknown,
UnreadState.HasNoUnread -> false
is UnreadState.HasUnread -> {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
if (positionOfReadMarker == null) {
it.timeline?.isLive == true && lastVisibleItem > 0
UnreadState.HasNoUnread -> false
is UnreadState.ReadMarkerNotLoaded -> true
is UnreadState.HasUnread -> {
if (it.canShowJumpToReadMarker) {
val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
val positionOfReadMarker = timelineEventController.getPositionOfReadMarker()
if (positionOfReadMarker == null) {
false
} else {
positionOfReadMarker > lastVisibleItem
}
} else {
positionOfReadMarker > lastVisibleItem
false
}
}
}
@ -1031,14 +1037,10 @@ class RoomDetailFragment @Inject constructor(
}
override fun onReadMarkerVisible() {
checkJumpToUnreadBanner()
updateJumpToReadMarkerViewVisibility()
roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
}
override fun onReadMarkerInvisible() {
checkJumpToUnreadBanner()
}
// AutocompleteUserPresenter.Callback
override fun onQueryUsers(query: CharSequence?) {
@ -1244,8 +1246,12 @@ class RoomDetailFragment @Inject constructor(
// JumpToReadMarkerView.Callback
override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
jumpToReadMarkerView.isVisible = false
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.functions.BiFunction
import io.reactivex.rxkotlin.subscribeBy
import io.reactivex.schedulers.Schedulers
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
import timber.log.Timber
@ -85,16 +86,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private val visibleEventsObservable = BehaviorRelay.create<RoomDetailAction.TimelineEventTurnsVisible>()
private val timelineSettings = if (userPreferencesProvider.shouldShowHiddenEvents()) {
TimelineSettings(30,
filterEdits = false,
filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
filterEdits = false,
filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DEBUG_DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
} else {
TimelineSettings(30,
filterEdits = true,
filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
filterEdits = true,
filterTypes = true,
allowedTypes = TimelineDisplayableEvents.DISPLAYABLE_TYPES,
buildReadReceipts = userPreferencesProvider.shouldShowReadReceipts())
}
private var timelineEvents = PublishRelay.create<List<TimelineEvent>>()
@ -179,6 +180,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun startTrackingUnreadMessages() {
trackUnreadMessages.set(true)
setState { copy(canShowJumpToReadMarker = false) }
}
private fun stopTrackingUnreadMessages() {
@ -188,6 +190,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}
mostRecentDisplayedEvent = null
}
setState { copy(canShowJumpToReadMarker = true) }
}
private fun handleEventInvisible(action: RoomDetailAction.TimelineEventTurnsInvisible) {
@ -220,23 +223,23 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
copy(
// Create a sendMode from a draft and retrieve the TimelineEvent
sendMode = when (draft) {
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text)
}
}
is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text)
}
}
is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text)
}
}
} ?: SendMode.REGULAR("")
is UserDraft.REGULAR -> SendMode.REGULAR(draft.text)
is UserDraft.QUOTE -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.QUOTE(timelineEvent, draft.text)
}
}
is UserDraft.REPLY -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.REPLY(timelineEvent, draft.text)
}
}
is UserDraft.EDIT -> {
room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent ->
SendMode.EDIT(timelineEvent, draft.text)
}
}
} ?: SendMode.REGULAR("")
)
}
}
@ -245,7 +248,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun handleTombstoneEvent(action: RoomDetailAction.HandleTombstoneEvent) {
val tombstoneContent = action.event.getClearContent().toModel<RoomTombstoneContent>()
?: return
?: return
val roomId = tombstoneContent.replacementRoom ?: ""
val isRoomJoined = session.getRoom(roomId)?.roomSummary()?.membership == Membership.JOIN
@ -383,7 +386,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
if (inReplyTo != null) {
// TODO check if same content?
room.getTimeLineEvent(inReplyTo)?.let {
@ -392,13 +395,13 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
} else {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val existingBody = messageContent?.body ?: ""
if (existingBody != action.text) {
room.editTextMessage(state.sendMode.timelineEvent.root.eventId ?: "",
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
action.text,
action.autoMarkdown)
messageContent?.type ?: MessageType.MSGTYPE_TEXT,
action.text,
action.autoMarkdown)
} else {
Timber.w("Same message content, do not send edition")
}
@ -409,7 +412,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
val textMsg = messageContent?.body
val finalText = legacyRiotQuoteText(textMsg, action.text.toString())
@ -525,7 +528,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
}
}
}
@ -715,7 +718,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
.filter { it.isNotEmpty() }
.subscribeBy(onNext = { actions ->
val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
?: return@subscribeBy
?: return@subscribeBy
val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
if (trackUnreadMessages.get()) {
if (globalMostRecentDisplayedEvent == null) {
@ -788,29 +791,33 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
private fun getUnreadState() {
Observable
.combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(
timelineEvents,
timelineEvents.observeOn(Schedulers.computation()),
room.rx().liveRoomSummary().unwrap(),
BiFunction { timelineEvents, roomSummary ->
computeUnreadState(timelineEvents, roomSummary)
}
)
.takeUntil {
it != UnreadState.Unknown
}
.subscribe { unreadState ->
setState {
copy(unreadState = unreadState)
// We don't want live update of unread so we skip when we already had a HasUnread or HasNoUnread
.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 {
setState { copy(unreadState = it) }
}
.disposeOnClear()
}
private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
if (events.isEmpty()) return UnreadState.Unknown
val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
?: return UnreadState.Unknown
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
?: return UnreadState.Unknown
?: return UnreadState.ReadMarkerNotLoaded(readMarkerIdSnapshot)
for (i in (firstDisplayableEventIndex - 1) downTo 0) {
val timelineEvent = events.getOrNull(i) ?: 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>) {
timelineEvents.accept(snapshot)
setState { copy(currentSnapshot = snapshot) }
}
override fun onCleared() {

View File

@ -44,7 +44,8 @@ sealed class SendMode(open val text: String) {
sealed class UnreadState {
object Unknown : 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(
@ -59,10 +60,8 @@ data class RoomDetailViewState(
val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE,
val highlightedEventId: String? = null,
val currentSnapshot: List<TimelineEvent> = emptyList(),
val hasMoreToLoadForward: Boolean = false,
val hasMoreToLoadBackward: Boolean = false,
val unreadState: UnreadState = UnreadState.Unknown
val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true
) : MvRxState {
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 {
fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
fun onReadMarkerVisible()
fun onReadMarkerInvisible()
}
interface UrlClickCallback {
@ -178,7 +177,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
synchronized(modelCache) {
for (i in 0 until modelCache.size) {
if (modelCache[i]?.eventId == viewState.highlightedEventId
|| modelCache[i]?.eventId == eventIdToHighlight) {
|| modelCache[i]?.eventId == eventIdToHighlight) {
modelCache[i] = null
}
}
@ -280,12 +279,12 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent,
items = items,
addDaySeparator = addDaySeparator,
currentPosition = currentPosition,
eventIdToHighlight = eventIdToHighlight,
callback = callback
nextEvent = nextEvent,
items = items,
addDaySeparator = addDaySeparator,
currentPosition = currentPosition,
eventIdToHighlight = eventIdToHighlight,
callback = callback
) {
requestModelBuild()
}
@ -297,7 +296,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? {
return when (currentUnreadState) {
is UnreadState.HasUnread -> {
if (event.root.eventId == currentUnreadState.eventId) {
if (event.root.eventId == currentUnreadState.firstUnreadEventId) {
TimelineReadMarkerItem_()
.also {
it.id("read_marker")
@ -307,8 +306,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
null
}
}
UnreadState.Unknown,
UnreadState.HasNoUnread -> null
else -> null
}
}
@ -359,6 +357,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val formattedDayModel: DaySeparatorItem? = 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

@ -47,7 +47,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
val nextDate = nextEvent?.root?.localDateTime()
val addDaySeparator = date.toLocalDate() != nextDate?.toLocalDate()
val isNextMessageReceivedMoreThanOneHourAgo = nextDate?.isBefore(date.minusMinutes(60))
?: false
?: false
val showInformation =
addDaySeparator

View File

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