Read marker: continue rework [WIP]
This commit is contained in:
parent
ab489df83d
commit
d9982076f9
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?)
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue