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(
|
||||
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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user