diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt index 96e1caf71b..08d34d3056 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultGetContextOfEventTask.kt @@ -26,7 +26,8 @@ internal interface GetContextOfEventTask : Task { - 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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt index 4411a039a5..b83240a681 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimeline.kt @@ -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) } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt index 4dfe3e5c45..f06697351e 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -30,6 +30,7 @@ data class EventContextResponse( @Json(name = "state") override val stateEvents: List = emptyList() ) : TokenChunkEvent { - override val events: List - get() = listOf(event) + override val events: List by lazy { + eventsAfter.reversed() + listOf(event) + eventsBefore + } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 8fb4d30b9f..d50b0c9f68 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -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)) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt index ccce70f33c..2dab9264e5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewModel.kt @@ -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() 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>() @@ -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() - ?: 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()?.relatesTo?.inReplyTo?.eventId - ?: state.sendMode.timelineEvent.root.content.toModel()?.relatesTo?.inReplyTo?.eventId + ?: state.sendMode.timelineEvent.root.content.toModel()?.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, 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, 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) { timelineEvents.accept(snapshot) - setState { copy(currentSnapshot = snapshot) } } override fun onCleared() { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt index 23971a93cd..a0be8fc9dc 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailViewState.kt @@ -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 = Uninitialized, val syncState: SyncState = SyncState.IDLE, val highlightedEventId: String? = null, - val currentSnapshot: List = 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) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt index d4447f5b05..e3e9cf7378 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/TimelineEventController.kt @@ -83,7 +83,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec interface ReadReceiptsCallback { fun onReadReceiptsClicked(readReceipts: List) 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) + } } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt index 34e34fc7de..784a180d00 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt @@ -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 diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt index 11b7d68923..69b2b24899 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt @@ -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() } } }