Handle epoxy timeline manually : get off the cache at the moment, will need to be improved

This commit is contained in:
ganfra 2018-10-31 11:22:42 +01:00
parent bc8055c3cd
commit ecf728aef8
10 changed files with 123 additions and 127 deletions

View File

@ -12,11 +12,13 @@ abstract class KotlinModel(
) : EpoxyModel<View>() { ) : EpoxyModel<View>() {


private var view: View? = null private var view: View? = null
private var onBindCallback: (() -> Unit)? = null


abstract fun bind() abstract fun bind()


override fun bind(view: View) { override fun bind(view: View) {
this.view = view this.view = view
onBindCallback?.invoke()
bind() bind()
} }


@ -24,6 +26,11 @@ abstract class KotlinModel(
this.view = null this.view = null
} }


fun onBind(lambda: (() -> Unit)?): KotlinModel {
onBindCallback = lambda
return this
}

override fun getDefaultLayout() = layoutRes override fun getDefaultLayout() = layoutRes


protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> { protected fun <V : View> bind(@IdRes id: Int) = object : ReadOnlyProperty<KotlinModel, V> {
@ -33,7 +40,7 @@ abstract class KotlinModel(
// be optimized with a map // be optimized with a map
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
return view?.findViewById(id) as V? 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

@ -4,15 +4,14 @@ import android.support.v7.util.DiffUtil
import im.vector.matrix.android.api.session.events.model.EnrichedEvent import im.vector.matrix.android.api.session.events.model.EnrichedEvent


class EventDiffUtilCallback : DiffUtil.ItemCallback<EnrichedEvent>() { class EventDiffUtilCallback : DiffUtil.ItemCallback<EnrichedEvent>() {

override fun areItemsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean { override fun areItemsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean {
return p0.root.eventId == p1.root.eventId return p0.root.eventId == p1.root.eventId
} }


override fun areContentsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean { override fun areContentsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean {
return p0.root == p1.root return p0.root == p1.root
&& p0.getMetaEvents() && p0.metadata == p1.metadata
.zip(p1.getMetaEvents()) { a, b ->
a.eventId == b.eventId
}.none { !it }
} }

} }

View File

@ -4,7 +4,6 @@ import android.arch.lifecycle.Observer
import android.arch.paging.PagedList import android.arch.paging.PagedList
import android.os.Bundle import android.os.Bundle
import android.support.v7.widget.LinearLayoutManager import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -20,7 +19,7 @@ import im.vector.riotredesign.features.home.RoomSummaryViewHelper
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject


class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback { class RoomDetailFragment : RiotFragment() {


companion object { companion object {


@ -34,7 +33,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {
private val matrix by inject<Matrix>() private val matrix by inject<Matrix>()
private val currentSession = matrix.currentSession!! private val currentSession = matrix.currentSession!!
private var roomId by FragmentArgumentDelegate<String>() private var roomId by FragmentArgumentDelegate<String>()
private val timelineAdapter = TimelineEventAdapter(this)
private val timelineEventController = TimelineEventController() private val timelineEventController = TimelineEventController()
private lateinit var room: Room private lateinit var room: Room


@ -61,18 +59,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {


private fun setupRecyclerView() { private fun setupRecyclerView() {
val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true) val layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, true)
layoutManager.stackFromEnd = true
timelineAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (layoutManager.findFirstVisibleItemPosition() == 0) {
layoutManager.scrollToPosition(0)
}

}
})
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
recyclerView.adapter = timelineAdapter recyclerView.setController(timelineEventController)
//recyclerView.setController(timelineEventController)
} }


private fun renderRoomSummary(roomSummary: RoomSummary?) { private fun renderRoomSummary(roomSummary: RoomSummary?) {
@ -90,16 +78,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {
} }


private fun renderEvents(events: PagedList<EnrichedEvent>?) { private fun renderEvents(events: PagedList<EnrichedEvent>?) {
timelineAdapter.submitList(events) timelineEventController.timeline = events
timelineEventController.requestModelBuild()
} }



override
fun onEventsListChanged(oldList: List<EnrichedEvent>?, newList: List<EnrichedEvent>?) {
if (oldList == null && newList != null) {
recyclerView.scrollToPosition(0)
}
}


} }

View File

@ -1,68 +0,0 @@
package im.vector.riotredesign.features.home.room.detail

import android.arch.paging.PagedList
import android.arch.paging.PagedListAdapter
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.MessageContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.riotredesign.R

/**
* Created by francois on 14/05/2018.
*/

class TimelineEventAdapter(private val callback: Callback? = null)
: PagedListAdapter<EnrichedEvent, TimelineEventAdapter.ViewHolder>(EventDiffUtilCallback()) {


private var currentList: List<EnrichedEvent>? = null

override fun onCreateViewHolder(parent: ViewGroup, position: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_event, parent, false)
return ViewHolder(view)
}

override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
val event = getItem(position)
viewHolder.bind(event)
}

class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {

val titleView = view.findViewById<TextView>(R.id.titleView)!!

fun bind(event: EnrichedEvent?) {
if (event == null) {
titleView.text = null
} else if (event.root.type == EventType.MESSAGE) {
val messageContent = event.root.content<MessageContent>()
val roomMember = event.getMetaEvents(EventType.STATE_ROOM_MEMBER).firstOrNull()?.content<RoomMember>()
if (messageContent == null || roomMember == null) {
titleView.text = null
} else {
val text = "${roomMember.displayName} : ${messageContent.body}"
titleView.text = text
}
} else {
titleView.text = event.root.toString()
}
}
}

override fun onCurrentListChanged(newList: PagedList<EnrichedEvent>?) {
callback?.onEventsListChanged(currentList, newList)
currentList = newList
}

interface Callback {
fun onEventsListChanged(oldList: List<EnrichedEvent>?, newList: List<EnrichedEvent>?)
}

}

View File

@ -1,29 +1,71 @@
package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import android.arch.paging.PagedList
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.paging.PagedListEpoxyController import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.MessageContent
import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_


class TimelineEventController : PagedListEpoxyController<Event>( private const val PREFETCH_DISTANCE = 5
diffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler()
class TimelineEventController : EpoxyController(
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) { ) {


override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { private val pagedListCallback = object : PagedList.Callback() {
return if (item == null) { override fun onChanged(position: Int, count: Int) {
LoadingItemModel_().id(-currentPosition) requestModelBuild()
} else { }
TimelineEventItem(item.toString()).id(item.eventId)
override fun onInserted(position: Int, count: Int) {
requestModelBuild()
}

override fun onRemoved(position: Int, count: Int) {
requestModelBuild()
} }
} }


init { var timeline: PagedList<EnrichedEvent>? = null
isDebugLoggingEnabled = true set(value) {
field?.removeWeakCallback(pagedListCallback)
field = value
field?.addWeakCallback(null, pagedListCallback)
}


override fun buildModels() {
buildModels(timeline)
} }


override fun onExceptionSwallowed(exception: RuntimeException) { private fun buildModels(data: List<EnrichedEvent>?) {
throw exception if (data.isNullOrEmpty()) {
return
}
data.forEachIndexed { index, enrichedEvent ->
val item = if (enrichedEvent.root.type == EventType.MESSAGE) {
val messageContent = enrichedEvent.root.content<MessageContent>()
val roomMember = enrichedEvent.getMetadata<Event>(EventType.STATE_ROOM_MEMBER)?.content<RoomMember>()
val title = "${roomMember?.displayName} : ${messageContent?.body}"
TimelineEventItem(title = title)
} else {
TimelineEventItem(title = enrichedEvent.toString())
}
item
.onBind { timeline?.loadAround(index) }
.id(enrichedEvent.root.eventId)
.addTo(this)
}

val isLastEvent = data.last().getMetadata<Boolean>(EnrichedEvent.IS_LAST_EVENT) ?: false
LoadingItemModel_()
.id("backward_loading_item")
.addIf(!isLastEvent, this)
} }


} }

View File

@ -5,14 +5,12 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.epoxy.KotlinModel import im.vector.riotredesign.core.epoxy.KotlinModel


data class TimelineEventItem( data class TimelineEventItem(
val title: String, val title: String
val listener: (() -> Unit)? = null
) : KotlinModel(R.layout.item_event) { ) : KotlinModel(R.layout.item_event) {


val titleView by bind<TextView>(R.id.titleView) val titleView by bind<TextView>(R.id.titleView)


override fun bind() { override fun bind() {
titleView.setOnClickListener { listener?.invoke() }
titleView.text = title titleView.text = title
} }
} }

View File

@ -2,7 +2,7 @@ package im.vector.matrix.android.api.session.events.model


data class EnrichedEvent(val root: Event) { data class EnrichedEvent(val root: Event) {


private val metaEventsByType = HashMap<String, ArrayList<Event>>() val metadata = HashMap<String, Any>()


fun enrichWith(events: List<Event>) { fun enrichWith(events: List<Event>) {
events.forEach { enrichWith(it) } events.forEach { enrichWith(it) }
@ -12,24 +12,22 @@ data class EnrichedEvent(val root: Event) {
if (event == null) { if (event == null) {
return return
} }
var currentEventsForType = metaEventsByType[event.type] enrichWith(event.type, event)
if (currentEventsForType == null) { }
currentEventsForType = ArrayList()
metaEventsByType[event.type] = currentEventsForType fun enrichWith(key: String, data: Any) {
if (!metadata.containsKey(key)) {
metadata[key] = data
} }
currentEventsForType.add(event)
} }


fun getMetaEvents(type: String): List<Event> { inline fun <reified T> getMetadata(key: String): T? {
return metaEventsByType[type] ?: emptyList() return metadata[key] as T?
} }


fun getMetaEvents(): List<Event> { companion object {
return metaEventsByType.values.flatten() const val IS_LAST_EVENT = "IS_LAST_EVENT"
} const val READ_RECEIPTS = "READ_RECEIPTS"

override fun toString(): String {
return super.toString()
} }


} }

View File

@ -0,0 +1,37 @@
package im.vector.matrix.android.internal.session.events.interceptor

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.findAllIncludingEvents
import im.vector.matrix.android.internal.database.query.where
import io.realm.Sort
import java.util.*


class IsLastEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor {

override fun canEnrich(event: EnrichedEvent): Boolean {
return true
}

override fun enrich(roomId: String, event: EnrichedEvent) {
monarchy.doWithRealm { realm ->
if (event.root.eventId == null) {
return@doWithRealm
}
val eventEntity = EventEntity.where(realm, event.root.eventId).findFirst()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(event.root.eventId)).firstOrNull()
if (eventEntity == null || chunkEntity == null) {
return@doWithRealm
}
val sortedChunkEvents = chunkEntity.events.where().sort(EventEntityFields.ORIGIN_SERVER_TS, Sort.ASCENDING).findAll()
val isLastEvent = chunkEntity.prevToken == null && sortedChunkEvents?.indexOf(eventEntity) == 0
event.enrichWith(EnrichedEvent.IS_LAST_EVENT, isLastEvent)
}
}

}

View File

@ -10,6 +10,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.last import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where



class MessageEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor { class MessageEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor {


override fun canEnrich(event: EnrichedEvent): Boolean { override fun canEnrich(event: EnrichedEvent): Boolean {

View File

@ -11,6 +11,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.events.interceptor.IsLastEventInterceptor
import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
import io.realm.Sort import io.realm.Sort


@ -23,6 +24,7 @@ class DefaultTimelineHolder(private val roomId: String,


init { init {
eventInterceptors.add(MessageEventInterceptor(monarchy)) eventInterceptors.add(MessageEventInterceptor(monarchy))
eventInterceptors.add(IsLastEventInterceptor(monarchy))
} }


override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> { override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> {