diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
index c03effd7ad..a71f5b7479 100644
--- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
+++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/timeline/Timeline.kt
@@ -30,10 +30,16 @@ package im.vector.matrix.android.api.session.room.timeline
  */
 interface Timeline {
 
-    var listener: Listener?
+    val timelineID: String
 
     val isLive: Boolean
 
+    fun addListener(listener: Listener): Boolean
+
+    fun removeListener(listener: Listener): Boolean
+
+    fun removeAllListeners()
+
     /**
      * This should be called before any other method after creating the timeline. It ensures the underlying database is open
      */
@@ -116,4 +122,5 @@ interface Timeline {
          */
         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 aa4bd42bf7..4411a039a5 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
@@ -81,14 +81,7 @@ internal class DefaultTimeline(
         val BACKGROUND_HANDLER = createBackgroundHandler("TIMELINE_DB_THREAD")
     }
 
-    override var listener: Timeline.Listener? = null
-        set(value) {
-            field = value
-            BACKGROUND_HANDLER.post {
-                postSnapshot()
-            }
-        }
-
+    private val listeners = ArrayList<Timeline.Listener>()
     private val isStarted = AtomicBoolean(false)
     private val isReady = AtomicBoolean(false)
     private val mainHandler = createUIHandler()
@@ -109,7 +102,7 @@ internal class DefaultTimeline(
     private val backwardsState = AtomicReference(State())
     private val forwardsState = AtomicReference(State())
 
-    private val timelineID = UUID.randomUUID().toString()
+    override val timelineID = UUID.randomUUID().toString()
 
     override val isLive
         get() = !hasMoreToLoad(Timeline.Direction.FORWARDS)
@@ -295,6 +288,20 @@ internal class DefaultTimeline(
         return hasMoreInCache(direction) || !hasReachedEnd(direction)
     }
 
+    override fun addListener(listener: Timeline.Listener) = synchronized(listeners) {
+        listeners.add(listener).also {
+            postSnapshot()
+        }
+    }
+
+    override fun removeListener(listener: Timeline.Listener) = synchronized(listeners) {
+        listeners.remove(listener)
+    }
+
+    override fun removeAllListeners() = synchronized(listeners) {
+        listeners.clear()
+    }
+
     // TimelineHiddenReadReceipts.Delegate
 
     override fun rebuildEvent(eventId: String, readReceipts: List<ReadReceipt>): Boolean {
@@ -487,9 +494,9 @@ internal class DefaultTimeline(
             return
         }
         val params = PaginationTask.Params(roomId = roomId,
-                from = token,
-                direction = direction.toPaginationDirection(),
-                limit = limit)
+                                           from = token,
+                                           direction = direction.toPaginationDirection(),
+                                           limit = limit)
 
         Timber.v("Should fetch $limit items $direction")
         cancelableBag += paginationTask
@@ -564,7 +571,7 @@ internal class DefaultTimeline(
             val timelineEvent = buildTimelineEvent(eventEntity)
 
             if (timelineEvent.isEncrypted()
-                    && timelineEvent.root.mxDecryptionResult == null) {
+                && timelineEvent.root.mxDecryptionResult == null) {
                 timelineEvent.root.eventId?.let { eventDecryptor.requestDecryption(it) }
             }
 
@@ -637,7 +644,13 @@ internal class DefaultTimeline(
             }
             updateLoadingStates(filteredEvents)
             val snapshot = createSnapshot()
-            val runnable = Runnable { listener?.onUpdated(snapshot) }
+            val runnable = Runnable {
+                synchronized(listeners) {
+                    listeners.forEach {
+                        it.onUpdated(snapshot)
+                    }
+                }
+            }
             debouncer.debounce("post_snapshot", runnable, 50)
         }
     }
diff --git a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
index abc2dd98f8..5ba482837e 100644
--- a/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
+++ b/vector/src/main/java/im/vector/riotx/core/ui/views/JumpToReadMarkerView.kt
@@ -34,7 +34,7 @@ class JumpToReadMarkerView @JvmOverloads constructor(
 ) : RelativeLayout(context, attrs, defStyleAttr) {
 
     interface Callback {
-        fun onJumpToReadMarkerClicked(readMarkerId: String)
+        fun onJumpToReadMarkerClicked()
         fun onClearReadMarkerClicked()
     }
 
@@ -44,24 +44,15 @@ class JumpToReadMarkerView @JvmOverloads constructor(
         setupView()
     }
 
-    private var readMarkerId: String? = null
-
     private fun setupView() {
         inflate(context, R.layout.view_jump_to_read_marker, this)
         setBackgroundColor(ContextCompat.getColor(context, R.color.notification_accent_color))
         jumpToReadMarkerLabelView.setOnClickListener {
-            readMarkerId?.also {
-                callback?.onJumpToReadMarkerClicked(it)
-            }
+            callback?.onJumpToReadMarkerClicked()
         }
         closeJumpToReadMarkerView.setOnClickListener {
             visibility = View.INVISIBLE
             callback?.onClearReadMarkerClicked()
         }
     }
-
-    fun render(show: Boolean, readMarkerId: String?) {
-        this.readMarkerId = readMarkerId
-        isInvisible = !show
-    }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt
deleted file mode 100644
index 98556cc7fa..0000000000
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/ReadMarkerHelper.kt
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
-
-  * Copyright 2019 New Vector Ltd
-  *
-  * Licensed under the Apache License, Version 2.0 (the "License");
-  * you may not use this file except in compliance with the License.
-  * You may obtain a copy of the License at
-  *
-  *     http://www.apache.org/licenses/LICENSE-2.0
-  *
-  * Unless required by applicable law or agreed to in writing, software
-  * distributed under the License is distributed on an "AS IS" BASIS,
-  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-  * See the License for the specific language governing permissions and
-  * limitations under the License.
-
- */
-package im.vector.riotx.features.home.room.detail
-
-import androidx.recyclerview.widget.LinearLayoutManager
-import im.vector.riotx.core.di.ScreenScope
-import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
-import javax.inject.Inject
-
-@ScreenScope
-class ReadMarkerHelper @Inject constructor() {
-
-    lateinit var timelineEventController: TimelineEventController
-    lateinit var layoutManager: LinearLayoutManager
-    var callback: Callback? = null
-    private var jumpToReadMarkerVisible = false
-    private var state: RoomDetailViewState? = null
-
-    fun updateWith(newState: RoomDetailViewState) {
-        state = newState
-        checkJumpToReadMarkerVisibility()
-    }
-
-    private fun checkJumpToReadMarkerVisibility() {
-        val nonNullState = this.state ?: return
-        val lastVisibleItem = layoutManager.findLastVisibleItemPosition()
-        val readMarkerId = nonNullState.asyncRoomSummary()?.readMarkerId
-        val newJumpToReadMarkerVisible = if (readMarkerId == null) {
-            false
-        } else {
-            val correctedReadMarkerId = nonNullState.timeline?.getFirstDisplayableEventId(readMarkerId)
-                    ?: readMarkerId
-            val positionOfReadMarker = timelineEventController.searchPositionOfEvent(correctedReadMarkerId)
-            if (positionOfReadMarker == null) {
-                nonNullState.timeline?.isLive == true && lastVisibleItem > 0
-            } else {
-                positionOfReadMarker > lastVisibleItem
-            }
-        }
-        if (newJumpToReadMarkerVisible != jumpToReadMarkerVisible) {
-            jumpToReadMarkerVisible = newJumpToReadMarkerVisible
-            callback?.onJumpToReadMarkerVisibilityUpdate(jumpToReadMarkerVisible, readMarkerId)
-        }
-    }
-
-    interface Callback {
-        fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?)
-    }
-}
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 e0c43b9e74..8fb4d30b9f 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
@@ -39,6 +39,7 @@ import androidx.core.text.buildSpannedString
 import androidx.core.util.Pair
 import androidx.core.view.ViewCompat
 import androidx.core.view.forEach
+import androidx.core.view.isVisible
 import androidx.recyclerview.widget.ItemTouchHelper
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.RecyclerView
@@ -57,7 +58,6 @@ import im.vector.matrix.android.api.permalinks.PermalinkFactory
 import im.vector.matrix.android.api.session.Session
 import im.vector.matrix.android.api.session.content.ContentAttachmentData
 import im.vector.matrix.android.api.session.events.model.Event
-import im.vector.matrix.android.api.session.events.model.LocalEcho
 import im.vector.matrix.android.api.session.room.model.Membership
 import im.vector.matrix.android.api.session.room.model.message.*
 import im.vector.matrix.android.api.session.room.send.SendState
@@ -145,8 +145,7 @@ class RoomDetailFragment @Inject constructor(
         val textComposerViewModelFactory: TextComposerViewModel.Factory,
         private val errorFormatter: ErrorFormatter,
         private val eventHtmlRenderer: EventHtmlRenderer,
-        private val vectorPreferences: VectorPreferences,
-        private val readMarkerHelper: ReadMarkerHelper
+        private val vectorPreferences: VectorPreferences
 ) :
         VectorBaseFragment(),
         TimelineEventController.Callback,
@@ -425,7 +424,8 @@ class RoomDetailFragment @Inject constructor(
         if (text != composerLayout.composerEditText.text.toString()) {
             // Ignore update to avoid saving a draft
             composerLayout.composerEditText.setText(text)
-            composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length ?: 0)
+            composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text?.length
+                    ?: 0)
         }
     }
 
@@ -474,13 +474,7 @@ class RoomDetailFragment @Inject constructor(
             it.dispatchTo(stateRestorer)
             it.dispatchTo(scrollOnNewMessageCallback)
             it.dispatchTo(scrollOnHighlightedEventCallback)
-        }
-        readMarkerHelper.timelineEventController = timelineEventController
-        readMarkerHelper.layoutManager = layoutManager
-        readMarkerHelper.callback = object : ReadMarkerHelper.Callback {
-            override fun onJumpToReadMarkerVisibilityUpdate(show: Boolean, readMarkerId: String?) {
-                jumpToReadMarkerView.render(show, readMarkerId)
-            }
+            checkJumpToUnreadBanner()
         }
         recyclerView.adapter = timelineEventController.adapter
 
@@ -526,6 +520,25 @@ class RoomDetailFragment @Inject constructor(
         }
     }
 
+    private fun checkJumpToUnreadBanner() = 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
+                    } else {
+                        positionOfReadMarker > lastVisibleItem
+                    }
+                }
+            }
+            jumpToReadMarkerView.isVisible = showJumpToUnreadBanner
+        }
+    }
+
     private fun updateJumpToBottomViewVisibility() {
         debouncer.debounce("jump_to_bottom_visibility", 250, Runnable {
             Timber.v("First visible: ${layoutManager.findFirstCompletelyVisibleItemPosition()}")
@@ -656,7 +669,6 @@ class RoomDetailFragment @Inject constructor(
     }
 
     private fun renderState(state: RoomDetailViewState) {
-        readMarkerHelper.updateWith(state)
         renderRoomSummary(state)
         val summary = state.asyncRoomSummary()
         val inviter = state.asyncInviter()
@@ -1018,10 +1030,15 @@ class RoomDetailFragment @Inject constructor(
                 .show(requireActivity().supportFragmentManager, "DISPLAY_READ_RECEIPTS")
     }
 
-    override fun onReadMarkerDisplayed() {
+    override fun onReadMarkerVisible() {
+        checkJumpToUnreadBanner()
         roomDetailViewModel.handle(RoomDetailAction.EnterTrackingUnreadMessagesState)
     }
 
+    override fun onReadMarkerInvisible() {
+        checkJumpToUnreadBanner()
+    }
+
     // AutocompleteUserPresenter.Callback
 
     override fun onQueryUsers(query: CharSequence?) {
@@ -1226,8 +1243,10 @@ class RoomDetailFragment @Inject constructor(
 
     // JumpToReadMarkerView.Callback
 
-    override fun onJumpToReadMarkerClicked(readMarkerId: String) {
-        roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(readMarkerId, false))
+    override fun onJumpToReadMarkerClicked() = withState(roomDetailViewModel) {
+        if (it.unreadState is UnreadState.HasUnread) {
+            roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(it.unreadState.eventId, false))
+        }
     }
 
     override fun onClearReadMarkerClicked() {
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 e1ff991797..ccce70f33c 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
@@ -22,6 +22,7 @@ import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.airbnb.mvrx.*
 import com.jakewharton.rxrelay2.BehaviorRelay
+import com.jakewharton.rxrelay2.PublishRelay
 import com.squareup.inject.assisted.Assisted
 import com.squareup.inject.assisted.AssistedInject
 import im.vector.matrix.android.api.MatrixCallback
@@ -35,11 +36,13 @@ import im.vector.matrix.android.api.session.file.FileService
 import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
 import im.vector.matrix.android.api.session.room.model.Membership
 import im.vector.matrix.android.api.session.room.model.RoomMember
+import im.vector.matrix.android.api.session.room.model.RoomSummary
 import im.vector.matrix.android.api.session.room.model.message.MessageContent
 import im.vector.matrix.android.api.session.room.model.message.MessageType
 import im.vector.matrix.android.api.session.room.model.message.getFileUrl
 import im.vector.matrix.android.api.session.room.model.tombstone.RoomTombstoneContent
 import im.vector.matrix.android.api.session.room.send.UserDraft
+import im.vector.matrix.android.api.session.room.timeline.Timeline
 import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
 import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
 import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
@@ -59,6 +62,8 @@ import im.vector.riotx.features.command.CommandParser
 import im.vector.riotx.features.command.ParsedCommand
 import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
 import im.vector.riotx.features.settings.VectorPreferences
+import io.reactivex.Observable
+import io.reactivex.functions.BiFunction
 import io.reactivex.rxkotlin.subscribeBy
 import org.commonmark.parser.Parser
 import org.commonmark.renderer.html.HtmlRenderer
@@ -72,7 +77,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                                                       private val vectorPreferences: VectorPreferences,
                                                       private val stringProvider: StringProvider,
                                                       private val session: Session
-) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState) {
+) : VectorViewModel<RoomDetailViewState, RoomDetailAction>(initialState), Timeline.Listener {
 
     private val room = session.getRoom(initialState.roomId)!!
     private val eventId = initialState.eventId
@@ -80,18 +85,19 @@ 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>>()
     private var timeline = room.createTimeline(eventId, timelineSettings)
 
     // Can be used for several actions, for a one shot result
@@ -125,13 +131,15 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     init {
-        getSnapshotOfReadMarkerId()
+        getUnreadState()
         observeSyncState()
         observeRoomSummary()
         observeEventDisplayedActions()
         observeSummaryState()
         observeDrafts()
+        observeUnreadState()
         room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
+        timeline.addListener(this)
         timeline.start()
         setState { copy(timeline = this@RoomDetailViewModel.timeline) }
     }
@@ -164,16 +172,16 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
             is RoomDetailAction.MarkAllAsRead                    -> handleMarkAllAsRead()
             is RoomDetailAction.ReportContent                    -> handleReportContent(action)
             is RoomDetailAction.IgnoreUser                       -> handleIgnoreUser(action)
-            is RoomDetailAction.EnterTrackingUnreadMessagesState -> handleEnterTrackingUnreadMessages()
-            is RoomDetailAction.ExitTrackingUnreadMessagesState  -> handleExitTrackingUnreadMessages()
+            is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
+            is RoomDetailAction.ExitTrackingUnreadMessagesState  -> stopTrackingUnreadMessages()
         }
     }
 
-    private fun handleEnterTrackingUnreadMessages() {
+    private fun startTrackingUnreadMessages() {
         trackUnreadMessages.set(true)
     }
 
-    private fun handleExitTrackingUnreadMessages() {
+    private fun stopTrackingUnreadMessages() {
         if (trackUnreadMessages.getAndSet(false)) {
             mostRecentDisplayedEvent?.root?.eventId?.also {
                 room.setReadMarker(it, callback = object : MatrixCallback<Unit> {})
@@ -212,23 +220,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("")
                         )
                     }
                 }
@@ -237,7 +245,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
@@ -375,7 +383,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 {
@@ -384,13 +392,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")
                         }
@@ -401,7 +409,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())
@@ -517,7 +525,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)))
             }
         }
     }
@@ -647,6 +655,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
     }
 
     private fun handleNavigateToEvent(action: RoomDetailAction.NavigateToEvent) {
+        stopTrackingUnreadMessages()
         val targetEventId: String = action.eventId
         val correctedEventId = timeline.getFirstDisplayableEventId(targetEventId) ?: targetEventId
         val indexOfEvent = timeline.getIndexOfEvent(correctedEventId)
@@ -705,7 +714,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 .buffer(1, TimeUnit.SECONDS)
                 .filter { it.isNotEmpty() }
                 .subscribeBy(onNext = { actions ->
-                    val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event ?: return@subscribeBy
+                    val bufferedMostRecentDisplayedEvent = actions.maxBy { it.event.displayIndex }?.event
+                                                           ?: return@subscribeBy
                     val globalMostRecentDisplayedEvent = mostRecentDisplayedEvent
                     if (trackUnreadMessages.get()) {
                         if (globalMostRecentDisplayedEvent == null) {
@@ -775,19 +785,53 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
                 }
     }
 
-    private fun getSnapshotOfReadMarkerId() {
-        room.rx().liveRoomSummary()
-                .unwrap()
-                .filter { it.readMarkerId != null }
-                .take(1)
-                .subscribe { roomSummary ->
+    private fun getUnreadState() {
+        Observable
+                .combineLatest<List<TimelineEvent>, RoomSummary, UnreadState>(
+                        timelineEvents,
+                        room.rx().liveRoomSummary().unwrap(),
+                        BiFunction { timelineEvents, roomSummary ->
+                            computeUnreadState(timelineEvents, roomSummary)
+                        }
+                )
+                .takeUntil {
+                    it != UnreadState.Unknown
+                }
+                .subscribe { unreadState ->
                     setState {
-                        copy(readMarkerIdSnapshot = roomSummary.readMarkerId)
+                        copy(unreadState = unreadState)
                     }
                 }
                 .disposeOnClear()
     }
 
+    private fun computeUnreadState(events: List<TimelineEvent>, roomSummary: RoomSummary): UnreadState {
+        val readMarkerIdSnapshot = roomSummary.readMarkerId ?: return UnreadState.Unknown
+        val firstDisplayableEventId = timeline.getFirstDisplayableEventId(readMarkerIdSnapshot)
+                                      ?: return UnreadState.Unknown
+        val firstDisplayableEventIndex = timeline.getIndexOfEvent(firstDisplayableEventId)
+                                         ?: return UnreadState.Unknown
+        for (i in (firstDisplayableEventIndex - 1) downTo 0) {
+            val timelineEvent = events.getOrNull(i) ?: return UnreadState.Unknown
+            val eventId = timelineEvent.root.eventId ?: return UnreadState.Unknown
+            val isFromMe = timelineEvent.root.senderId == session.myUserId
+            if (!isFromMe) {
+                return UnreadState.HasUnread(eventId)
+            }
+        }
+        return UnreadState.HasNoUnread
+    }
+
+
+    private fun observeUnreadState() {
+        selectSubscribe(RoomDetailViewState::unreadState) {
+            Timber.v("Unread state: $it")
+            if (it is UnreadState.HasNoUnread) {
+                startTrackingUnreadMessages()
+            }
+        }
+    }
+
     private fun observeSummaryState() {
         asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
             if (summary.membership == Membership.INVITE) {
@@ -803,8 +847,14 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
         }
     }
 
+    override fun onUpdated(snapshot: List<TimelineEvent>) {
+        timelineEvents.accept(snapshot)
+        setState { copy(currentSnapshot = snapshot) }
+    }
+
     override fun onCleared() {
         timeline.dispose()
+        timeline.removeAllListeners()
         super.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 e476545aa8..23971a93cd 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
@@ -41,6 +41,12 @@ sealed class SendMode(open val text: String) {
     data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text)
 }
 
+sealed class UnreadState {
+    object Unknown : UnreadState()
+    object HasNoUnread : UnreadState()
+    data class HasUnread(val eventId: String) : UnreadState()
+}
+
 data class RoomDetailViewState(
         val roomId: String,
         val eventId: String?,
@@ -53,7 +59,10 @@ data class RoomDetailViewState(
         val tombstoneEventHandling: Async<String> = Uninitialized,
         val syncState: SyncState = SyncState.IDLE,
         val highlightedEventId: String? = null,
-        val readMarkerIdSnapshot: String? = null
+        val currentSnapshot: List<TimelineEvent> = emptyList(),
+        val hasMoreToLoadForward: Boolean = false,
+        val hasMoreToLoadBackward: Boolean = false,
+        val unreadState: UnreadState = UnreadState.Unknown
 ) : 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 180deb998b..d4447f5b05 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
@@ -33,6 +33,7 @@ import im.vector.riotx.core.date.VectorDateFormatter
 import im.vector.riotx.core.epoxy.LoadingItem_
 import im.vector.riotx.core.extensions.localDateTime
 import im.vector.riotx.features.home.room.detail.RoomDetailViewState
+import im.vector.riotx.features.home.room.detail.UnreadState
 import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
 import im.vector.riotx.features.home.room.detail.timeline.factory.TimelineItemFactory
 import im.vector.riotx.features.home.room.detail.timeline.helper.*
@@ -40,7 +41,6 @@ import im.vector.riotx.features.home.room.detail.timeline.item.*
 import im.vector.riotx.features.media.ImageContentRenderer
 import im.vector.riotx.features.media.VideoContentRenderer
 import org.threeten.bp.LocalDateTime
-import timber.log.Timber
 import javax.inject.Inject
 
 class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
@@ -82,7 +82,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
 
     interface ReadReceiptsCallback {
         fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>)
-        fun onReadMarkerDisplayed()
+        fun onReadMarkerVisible()
+        fun onReadMarkerInvisible()
     }
 
     interface UrlClickCallback {
@@ -97,7 +98,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
     private var currentSnapshot: List<TimelineEvent> = emptyList()
     private var inSubmitList: Boolean = false
     private var timeline: Timeline? = null
-    private var readMarkerIdSnapshot: String? = null
+    private var unreadState: UnreadState = UnreadState.Unknown
+    private var positionOfReadMarker: Int? = null
+    private var eventIdToHighlight: String? = null
 
     var callback: Callback? = null
 
@@ -150,6 +153,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
 
     // Update position when we are building new items
     override fun intercept(models: MutableList<EpoxyModel<*>>) {
+        positionOfReadMarker = null
         adapterPositionMapping.clear()
         models.forEachIndexed { index, epoxyModel ->
             if (epoxyModel is BaseEventItem) {
@@ -157,19 +161,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                     adapterPositionMapping[it] = index
                 }
             }
+            if (epoxyModel is TimelineReadMarkerItem) {
+                positionOfReadMarker = index
+            }
         }
     }
 
     fun update(viewState: RoomDetailViewState) {
-        if (timeline != viewState.timeline) {
+        if (timeline?.timelineID != viewState.timeline?.timelineID) {
             timeline = viewState.timeline
-            timeline?.listener = this
-            // Clear cache
-            synchronized(modelCache) {
-                for (i in 0 until modelCache.size) {
-                    modelCache[i] = null
-                }
-            }
+            timeline?.addListener(this)
         }
         var requestModelBuild = false
         if (eventIdToHighlight != viewState.highlightedEventId) {
@@ -177,7 +178,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
                     }
                 }
@@ -185,8 +186,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             eventIdToHighlight = viewState.highlightedEventId
             requestModelBuild = true
         }
-        if (this.readMarkerIdSnapshot != viewState.readMarkerIdSnapshot) {
-            this.readMarkerIdSnapshot = viewState.readMarkerIdSnapshot
+        if (this.unreadState != viewState.unreadState) {
+            this.unreadState = viewState.unreadState
             requestModelBuild = true
         }
         if (requestModelBuild) {
@@ -194,8 +195,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         }
     }
 
-    private var eventIdToHighlight: String? = null
-
     override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
         super.onAttachedToRecyclerView(recyclerView)
         timelineMediaSizeProvider.recyclerView = recyclerView
@@ -250,7 +249,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
                     } else {
                         it.eventModel
                     }
-                    listOf(eventModel, it?.mergedHeaderModel, it?.formattedDayModel, it?.readMarkerModel)
+                    listOf(eventModel, it?.mergedHeaderModel, it?.readMarkerModel, it?.formattedDayModel)
                 }
                 .flatten()
                 .filterNotNull()
@@ -260,31 +259,17 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         if (modelCache.isEmpty()) {
             return
         }
-        val displayableReadMarkerId = computeDisplayableReadMarkerId()
+        val currentUnreadState = this.unreadState
         (0 until modelCache.size).forEach { position ->
             // Should be build if not cached or if cached but contains additional models
             // We then are sure we always have items up to date.
-            if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild() == true) {
-                modelCache[position] = buildCacheItem(position, currentSnapshot, displayableReadMarkerId)
+            if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild(currentUnreadState) == true) {
+                modelCache[position] = buildCacheItem(position, currentSnapshot, currentUnreadState)
             }
         }
     }
 
-    private fun computeDisplayableReadMarkerId(): String? {
-        val readMarkerIdSnapshot = this.readMarkerIdSnapshot ?: return null
-        val firstDisplayableEventId = timeline?.getFirstDisplayableEventId(readMarkerIdSnapshot) ?: return null
-        val firstDisplayableEventIndex = timeline?.getIndexOfEvent(firstDisplayableEventId) ?: return null
-        for (i in (firstDisplayableEventIndex - 1) downTo 0) {
-            val timelineEvent = currentSnapshot.getOrNull(i) ?: return null
-            val isFromMe = timelineEvent.root.senderId == session.myUserId
-            if (!isFromMe) {
-                return timelineEvent.root.eventId
-            }
-        }
-        return null
-    }
-
-    private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>, displayableReadMarkerId: String?): CacheItemData {
+    private fun buildCacheItem(currentPosition: Int, items: List<TimelineEvent>, currentUnreadState: UnreadState): CacheItemData {
         val event = items[currentPosition]
         val nextEvent = items.nextOrNull(currentPosition)
         val date = event.root.localDateTime()
@@ -295,29 +280,35 @@ 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()
         }
         val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
-        val readMarkerItem = buildReadMarkerItem(event, displayableReadMarkerId)
+        val readMarkerItem = buildReadMarkerItem(event, currentUnreadState)
         return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem, readMarkerItem)
     }
 
-    private fun buildReadMarkerItem(event: TimelineEvent, displayableReadMarkerId: String?): TimelineReadMarkerItem? {
-        return if (event.root.eventId == displayableReadMarkerId) {
-            TimelineReadMarkerItem_()
-                    .also {
-                        it.id("read_marker")
-                        it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
-                    }
-        } else {
-            null
+    private fun buildReadMarkerItem(event: TimelineEvent, currentUnreadState: UnreadState): TimelineReadMarkerItem? {
+        return when (currentUnreadState) {
+            is UnreadState.HasUnread -> {
+                if (event.root.eventId == currentUnreadState.eventId) {
+                    TimelineReadMarkerItem_()
+                            .also {
+                                it.id("read_marker")
+                                it.setOnVisibilityStateChanged(ReadMarkerVisibilityStateChangedListener(callback))
+                            }
+                } else {
+                    null
+                }
+            }
+            UnreadState.Unknown,
+            UnreadState.HasNoUnread  -> null
         }
     }
 
@@ -354,6 +345,10 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
         return adapterPositionMapping[eventId]
     }
 
+    fun getPositionOfReadMarker(): Int? = synchronized(modelCache) {
+        return positionOfReadMarker
+    }
+
     fun isLoadingForward() = showingForwardLoader
 
     private data class CacheItemData(
@@ -364,6 +359,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
             val formattedDayModel: DaySeparatorItem? = null,
             val readMarkerModel: TimelineReadMarkerItem? = null
     ) {
-        fun shouldTriggerBuild() = mergedHeaderModel != null || formattedDayModel != null
+        fun shouldTriggerBuild(unreadState: UnreadState) = mergedHeaderModel != null || formattedDayModel != null || readMarkerModel != null || (unreadState is UnreadState.HasUnread && unreadState.eventId == eventId)
     }
 }
diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
similarity index 91%
rename from vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt
rename to vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
index 7efbce0073..11b7d68923 100644
--- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineEventVisibilityStateChangedListener.kt
+++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/timeline/helper/TimelineVisibilityStateChangedListeners.kt
@@ -24,12 +24,11 @@ import im.vector.riotx.features.home.room.detail.timeline.TimelineEventControlle
 class ReadMarkerVisibilityStateChangedListener(private val callback: TimelineEventController.Callback?)
     : VectorEpoxyModel.OnVisibilityStateChangedListener {
 
-    private var dispatched: Boolean = false
-
     override fun onVisibilityStateChanged(visibilityState: Int) {
-        if (visibilityState == VisibilityState.VISIBLE && !dispatched) {
-            dispatched = true
-            callback?.onReadMarkerDisplayed()
+        if (visibilityState == VisibilityState.VISIBLE) {
+            callback?.onReadMarkerVisible()
+        } else if (visibilityState == VisibilityState.INVISIBLE) {
+            callback?.onReadMarkerInvisible()
         }
     }
 }