diff --git a/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt b/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt index b69014ee..e94b7d04 100644 --- a/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt +++ b/app/src/main/java/im/vector/riotredesign/core/epoxy/KotlinModel.kt @@ -12,11 +12,13 @@ abstract class KotlinModel( ) : EpoxyModel() { private var view: View? = null + private var onBindCallback: (() -> Unit)? = null abstract fun bind() override fun bind(view: View) { this.view = view + onBindCallback?.invoke() bind() } @@ -24,6 +26,11 @@ abstract class KotlinModel( this.view = null } + fun onBind(lambda: (() -> Unit)?): KotlinModel { + onBindCallback = lambda + return this + } + override fun getDefaultLayout() = layoutRes protected fun bind(@IdRes id: Int) = object : ReadOnlyProperty { @@ -33,7 +40,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.") } } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/EventDiffUtilCallback.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/EventDiffUtilCallback.kt index 5990a23e..4727ddf5 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/EventDiffUtilCallback.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/EventDiffUtilCallback.kt @@ -4,15 +4,14 @@ import android.support.v7.util.DiffUtil import im.vector.matrix.android.api.session.events.model.EnrichedEvent class EventDiffUtilCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean { return p0.root.eventId == p1.root.eventId } override fun areContentsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean { return p0.root == p1.root - && p0.getMetaEvents() - .zip(p1.getMetaEvents()) { a, b -> - a.eventId == b.eventId - }.none { !it } + && p0.metadata == p1.metadata } + } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt index efe9fcad..b2fd9987 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailFragment.kt @@ -4,7 +4,6 @@ import android.arch.lifecycle.Observer import android.arch.paging.PagedList import android.os.Bundle import android.support.v7.widget.LinearLayoutManager -import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -20,7 +19,7 @@ import im.vector.riotredesign.features.home.RoomSummaryViewHelper import kotlinx.android.synthetic.main.fragment_room_detail.* import org.koin.android.ext.android.inject -class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback { +class RoomDetailFragment : RiotFragment() { companion object { @@ -34,7 +33,6 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback { private val matrix by inject() private val currentSession = matrix.currentSession!! private var roomId by FragmentArgumentDelegate() - private val timelineAdapter = TimelineEventAdapter(this) private val timelineEventController = TimelineEventController() private lateinit var room: Room @@ -61,18 +59,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback { private fun setupRecyclerView() { 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.adapter = timelineAdapter - //recyclerView.setController(timelineEventController) + recyclerView.setController(timelineEventController) } private fun renderRoomSummary(roomSummary: RoomSummary?) { @@ -90,16 +78,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback { } private fun renderEvents(events: PagedList?) { - timelineAdapter.submitList(events) + timelineEventController.timeline = events + timelineEventController.requestModelBuild() } - - override - fun onEventsListChanged(oldList: List?, newList: List?) { - if (oldList == null && newList != null) { - recyclerView.scrollToPosition(0) - } - } - - } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventAdapter.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventAdapter.kt deleted file mode 100644 index d0946b28..00000000 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventAdapter.kt +++ /dev/null @@ -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(EventDiffUtilCallback()) { - - - private var currentList: List? = 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(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() - val roomMember = event.getMetaEvents(EventType.STATE_ROOM_MEMBER).firstOrNull()?.content() - 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?) { - callback?.onEventsListChanged(currentList, newList) - currentList = newList - } - - interface Callback { - fun onEventsListChanged(oldList: List?, newList: List?) - } - -} - diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventController.kt index d2ea9fe0..1f98aefa 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventController.kt @@ -1,29 +1,71 @@ package im.vector.riotredesign.features.home.room.detail +import android.arch.paging.PagedList import com.airbnb.epoxy.EpoxyAsyncUtil -import com.airbnb.epoxy.EpoxyModel -import com.airbnb.epoxy.paging.PagedListEpoxyController +import com.airbnb.epoxy.EpoxyController +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.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_ -class TimelineEventController : PagedListEpoxyController( - diffingHandler = EpoxyAsyncUtil.getAsyncBackgroundHandler() +private const val PREFETCH_DISTANCE = 5 + +class TimelineEventController : EpoxyController( + EpoxyAsyncUtil.getAsyncBackgroundHandler(), + EpoxyAsyncUtil.getAsyncBackgroundHandler() ) { - override fun buildItemModel(currentPosition: Int, item: Event?): EpoxyModel<*> { - return if (item == null) { - LoadingItemModel_().id(-currentPosition) - } else { - TimelineEventItem(item.toString()).id(item.eventId) + private val pagedListCallback = object : PagedList.Callback() { + override fun onChanged(position: Int, count: Int) { + requestModelBuild() + } + + override fun onInserted(position: Int, count: Int) { + requestModelBuild() + } + + override fun onRemoved(position: Int, count: Int) { + requestModelBuild() } } - init { - isDebugLoggingEnabled = true + var timeline: PagedList? = null + set(value) { + field?.removeWeakCallback(pagedListCallback) + field = value + field?.addWeakCallback(null, pagedListCallback) + } + + + override fun buildModels() { + buildModels(timeline) } - override fun onExceptionSwallowed(exception: RuntimeException) { - throw exception + private fun buildModels(data: List?) { + if (data.isNullOrEmpty()) { + return + } + data.forEachIndexed { index, enrichedEvent -> + val item = if (enrichedEvent.root.type == EventType.MESSAGE) { + val messageContent = enrichedEvent.root.content() + val roomMember = enrichedEvent.getMetadata(EventType.STATE_ROOM_MEMBER)?.content() + 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(EnrichedEvent.IS_LAST_EVENT) ?: false + LoadingItemModel_() + .id("backward_loading_item") + .addIf(!isLastEvent, this) } } \ No newline at end of file diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventItem.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventItem.kt index c71fbf89..a280a6bc 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventItem.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/TimelineEventItem.kt @@ -5,14 +5,12 @@ import im.vector.riotredesign.R import im.vector.riotredesign.core.epoxy.KotlinModel data class TimelineEventItem( - val title: String, - val listener: (() -> Unit)? = null + val title: String ) : KotlinModel(R.layout.item_event) { val titleView by bind(R.id.titleView) override fun bind() { - titleView.setOnClickListener { listener?.invoke() } titleView.text = title } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt index ba7ff484..b1c13120 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/events/model/EnrichedEvent.kt @@ -2,7 +2,7 @@ package im.vector.matrix.android.api.session.events.model data class EnrichedEvent(val root: Event) { - private val metaEventsByType = HashMap>() + val metadata = HashMap() fun enrichWith(events: List) { events.forEach { enrichWith(it) } @@ -12,24 +12,22 @@ data class EnrichedEvent(val root: Event) { if (event == null) { return } - var currentEventsForType = metaEventsByType[event.type] - if (currentEventsForType == null) { - currentEventsForType = ArrayList() - metaEventsByType[event.type] = currentEventsForType + enrichWith(event.type, event) + } + + fun enrichWith(key: String, data: Any) { + if (!metadata.containsKey(key)) { + metadata[key] = data } - currentEventsForType.add(event) } - fun getMetaEvents(type: String): List { - return metaEventsByType[type] ?: emptyList() + inline fun getMetadata(key: String): T? { + return metadata[key] as T? } - fun getMetaEvents(): List { - return metaEventsByType.values.flatten() - } - - override fun toString(): String { - return super.toString() + companion object { + const val IS_LAST_EVENT = "IS_LAST_EVENT" + const val READ_RECEIPTS = "READ_RECEIPTS" } } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/IsLastEventInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/IsLastEventInterceptor.kt new file mode 100644 index 00000000..24154883 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/IsLastEventInterceptor.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/MessageEventInterceptor.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/MessageEventInterceptor.kt index 5b2d80fb..1d50cdb2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/MessageEventInterceptor.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/events/interceptor/MessageEventInterceptor.kt @@ -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.where + class MessageEventInterceptor(val monarchy: Monarchy) : EnrichedEventInterceptor { override fun canEnrich(event: EnrichedEvent): Boolean { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt index 4028ecdd..dacd70a2 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/DefaultTimelineHolder.kt @@ -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.EventEntityFields 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 io.realm.Sort @@ -23,6 +24,7 @@ class DefaultTimelineHolder(private val roomId: String, init { eventInterceptors.add(MessageEventInterceptor(monarchy)) + eventInterceptors.add(IsLastEventInterceptor(monarchy)) } override fun liveTimeline(): LiveData> {