Timeline : send read-receipt when scrolling. Still need to handle read marker.

This commit is contained in:
ganfra
2019-01-30 18:39:54 +01:00
parent bfaab52b41
commit 6113bba703
17 changed files with 198 additions and 49 deletions

View File

@ -16,10 +16,11 @@
package im.vector.riotredesign.core.epoxy
import android.view.View
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import android.view.View
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
@ -29,6 +30,7 @@ abstract class KotlinModel(
private var view: View? = null
private var onBindCallback: (() -> Unit)? = null
private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener<KotlinModel, View>? = null
abstract fun bind()
@ -47,6 +49,16 @@ abstract class KotlinModel(
return this
}
override fun onVisibilityStateChanged(visibilityState: Int, view: View) {
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState)
super.onVisibilityStateChanged(visibilityState, view)
}
fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener<KotlinModel, View>): KotlinModel {
this.onModelVisibilityStateChangedListener = listener
return this
}
override fun getDefaultLayout() = layoutRes
protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
@ -56,7 +68,7 @@ abstract class KotlinModel(
// be optimized with a map
@Suppress("UNCHECKED_CAST")
return view?.findViewById(id) as V?
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
?: throw IllegalStateException("View ID $id for '${property.name}' not found.")
}
}
}

View File

@ -16,9 +16,12 @@
package im.vector.riotredesign.features.home.room.detail
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
sealed class RoomDetailActions {
data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()
}

View File

@ -23,9 +23,11 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable
@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
override fun onResume() {
super.onResume()
roomDetailViewModel.accept(RoomDetailActions.IsDisplayed)
roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
}
private fun setupToolbar() {
@ -86,6 +88,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
}
private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager
@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) {
composerEditText.text = null
roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage))
roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
}
}
}
@ -143,4 +147,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
homePermalinkHandler.launch(url)
}
override fun onEventVisible(event: TimelineEvent, index: Int) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
}
}

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
@ -25,7 +26,9 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit
class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session,
@ -36,6 +39,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private val roomId = initialState.roomId
private val eventId = initialState.eventId
private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()
companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {
@JvmStatic
@ -49,14 +54,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
init {
observeRoomSummary()
observeTimeline()
observeDisplayedEvents()
room.loadRoomMembersIfNeeded()
room.markLatestAsRead(callback = object : MatrixCallback<Void> {})
}
fun accept(action: RoomDetailActions) {
fun process(action: RoomDetailActions) {
when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId)
is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
}
}
@ -66,6 +72,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
}
private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action)
}
private fun handleIsDisplayed() {
visibleRoomHolder.setVisibleRoom(roomId)
}
private fun observeDisplayedEvents() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
displayedEventsObservable.hide()
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy { actions ->
val mostRecentEvent = actions.minBy { it.index }
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
}
}
.disposeOnClear()
}
private fun observeRoomSummary() {
room.rx().liveRoomSummary()
.execute { async ->

View File

@ -16,12 +16,16 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
@ -74,6 +78,11 @@ class TimelineEventController(private val roomId: String,
timelineItemFactory.create(event, nextEvent, callback)?.also {
it.id(event.localId)
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event, currentPosition)
}
})
epoxyModels.add(it)
}
if (addDaySeparator) {
@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String,
interface Callback {
fun onEventVisible(event: TimelineEvent, index: Int)
fun onUrlClicked(url: String)
}

View File

@ -16,9 +16,9 @@
package im.vector.riotredesign.features.home.room.detail.timeline
import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory,
@ -28,14 +28,14 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?): EpoxyModel<*>? {
callback: TimelineEventController.Callback?): KotlinModel? {
return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
else -> defaultItemFactory.create(event)
else -> defaultItemFactory.create(event)
}
}