Add a way to enrich event with useful data to be displayed.

This commit is contained in:
ganfra 2018-10-22 19:36:29 +02:00
parent 0f4d15e488
commit 279241974a
18 changed files with 232 additions and 85 deletions

View File

@ -1,14 +1,18 @@
package im.vector.riotredesign.features.home

import android.support.v7.util.DiffUtil
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EnrichedEvent

class EventDiffUtilCallback : DiffUtil.ItemCallback<Event>() {
override fun areItemsTheSame(p0: Event, p1: Event): Boolean {
return p0.eventId == p1.eventId
class EventDiffUtilCallback : DiffUtil.ItemCallback<EnrichedEvent>() {
override fun areItemsTheSame(p0: EnrichedEvent, p1: EnrichedEvent): Boolean {
return p0.core.eventId == p1.core.eventId
}

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

View File

@ -9,7 +9,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.Room
import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment
@ -46,7 +46,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {
room.liveTimeline().observe(this, Observer { renderEvents(it) })
}

private fun renderEvents(events: PagedList<Event>?) {
private fun renderEvents(events: PagedList<EnrichedEvent>?) {
timelineAdapter.submitList(events)
}

@ -65,7 +65,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventAdapter.Callback {
//recyclerView.setController(timelineEventController)
}

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

View File

@ -7,7 +7,10 @@ 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.Event
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

/**
@ -15,10 +18,10 @@ import im.vector.riotredesign.R
*/

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


private var currentList: List<Event>? = null
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)
@ -30,27 +33,33 @@ class TimelineEventAdapter(private val callback: Callback? = null)
viewHolder.bind(event)
}


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

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

fun bind(event: Event?) {
if (event == null) {

fun bind(event: EnrichedEvent?) {
if (event == null || event.core.type != EventType.MESSAGE) {
titleView.text = null
} else {
titleView.text = event.toString()
val messageContent = event.core.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
}
}
}
}

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

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

}

View File

@ -13,7 +13,7 @@ class TimelineEventController : PagedListEpoxyController<Event>(
return if (item == null) {
LoadingItemModel_().id(-currentPosition)
} else {
TimelineEventItem(item.toString()).id(currentPosition)
TimelineEventItem(item.toString()).id(item.eventId)
}
}


View File

@ -0,0 +1,12 @@
package im.vector.matrix.android.api.session.events.interceptor

import im.vector.matrix.android.api.session.events.model.EnrichedEvent

interface EnrichedEventInterceptor {

fun enrich(roomId: String, event: EnrichedEvent)

fun canEnrich(event: EnrichedEvent): Boolean

}

View File

@ -0,0 +1,29 @@
package im.vector.matrix.android.api.session.events.interceptor

import com.zhuinden.monarchy.Monarchy
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.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
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 {
return event.core.type == EventType.MESSAGE
}

override fun enrich(roomId: String, event: EnrichedEvent) {
monarchy.doWithRealm { realm ->
val roomMember = EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.equalTo("stateKey", event.core.sender)
.last(from = event.core.originServerTs)
?.asDomain()
event.enrichWith(roomMember)
}
}


}

View File

@ -0,0 +1,35 @@
package im.vector.matrix.android.api.session.events.model

data class EnrichedEvent(val core: Event) {

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

fun enrichWith(events: List<Event>) {
events.forEach { enrichWith(it) }
}

fun enrichWith(event: Event?) {
if (event == null) {
return
}
var currentEventsForType = metaEventsByType[event.type]
if (currentEventsForType == null) {
currentEventsForType = ArrayList()
metaEventsByType[event.type] = currentEventsForType
}
currentEventsForType.add(event)
}

fun getMetaEvents(type: String): List<Event> {
return metaEventsByType[type] ?: emptyList()
}

fun getMetaEvents(): List<Event> {
return metaEventsByType.values.flatten()
}

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

}

View File

@ -2,12 +2,12 @@ package im.vector.matrix.android.api.session.room

import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EnrichedEvent

interface Room {

val roomId: String

fun liveTimeline(): LiveData<PagedList<Event>>
fun liveTimeline(): LiveData<PagedList<EnrichedEvent>>

}

View File

@ -0,0 +1,14 @@
package im.vector.matrix.android.api.session.room.model

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class MessageContent(

@Json(name = "msgtype") val type: String? = null,
@Json(name = "body") val body: String? = null,
@Json(name = "format") val format: String? = null,
@Json(name = "formatted_body") val formattedBody: String? = null

)

View File

@ -0,0 +1,17 @@
package im.vector.matrix.android.api.session.room.model

object MessageType {

val MSGTYPE_TEXT = "m.text"
val MSGTYPE_EMOTE = "m.emote"
val MSGTYPE_NOTICE = "m.notice"
val MSGTYPE_IMAGE = "m.image"
val MSGTYPE_AUDIO = "m.audio"
val MSGTYPE_VIDEO = "m.video"
val MSGTYPE_LOCATION = "m.location"
val MSGTYPE_FILE = "m.file"
val FORMAT_MATRIX_HTML = "org.matrix.custom.html"
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field
val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
}

View File

@ -7,9 +7,9 @@ import im.vector.matrix.android.api.session.events.model.UnsignedData
@JsonClass(generateAdapter = true)
data class RoomMember(
@Json(name = "membership") val membership: Membership,
@Json(name = "display_name") val displayDame: String? = null,
@Json(name = "displayname") val displayName: String? = null,
@Json(name = "avatar_url") val avatarUrl: String? = null,
@Json(name = "is_direct") val isDirect: Boolean = false,
@Json(name = "third_party_invite") val thirdPartyInvite: Invite? = null,
@Json(name = "unsigned_data") val unsignedData: UnsignedData? = null
@Json(name = "unsigned") val unsignedData: UnsignedData? = null
)

View File

@ -1,32 +1,26 @@
package im.vector.matrix.android.internal.database.query

import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.EventEntity
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.RealmResults
import io.realm.Sort

fun EventEntity.Companion.where(realm: Realm, roomId: String): RealmQuery<EventEntity> {
return realm.where(EventEntity::class.java)
.equalTo("chunk.room.roomId", roomId)
}

fun EventEntity.Companion.where(realm: Realm, chunk: ChunkEntity?): RealmQuery<EventEntity> {
fun EventEntity.Companion.where(realm: Realm, roomId: String, type: String? = null): RealmQuery<EventEntity> {
var query = realm.where(EventEntity::class.java)
if (chunk?.prevToken != null) {
query = query.equalTo("chunk.prevToken", chunk.prevToken)
}
if (chunk?.nextToken != null) {
query = query.equalTo("chunk.nextToken", chunk.nextToken)
.equalTo("chunk.room.roomId", roomId)
if (type != null) {
query = query.equalTo("type", type)
}
return query
}

fun RealmResults<EventEntity>.getLast(type: String? = null): EventEntity? {
var query = this.where().sort("originServerTs", Sort.DESCENDING)
if (type != null) {
query = query.equalTo("type", type)

fun RealmQuery<EventEntity>.last(from: Long? = null): EventEntity? {
var query = this
if (from != null) {
query = query.lessThanOrEqualTo("originServerTs", from)
}
return query.findFirst()
}
return query
.sort("originServerTs", Sort.DESCENDING)
.findFirst()
}

View File

@ -4,9 +4,11 @@ import android.arch.lifecycle.LiveData
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.interceptor.EnrichedEventInterceptor
import im.vector.matrix.android.api.session.events.interceptor.MessageEventInterceptor
import im.vector.matrix.android.api.session.events.model.EnrichedEvent
import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.database.mapper.EventMapper
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.query.where
import im.vector.matrix.android.internal.session.room.timeline.PaginationRequest
@ -23,8 +25,13 @@ data class DefaultRoom(
private val paginationRequest by inject<PaginationRequest>()
private val monarchy by inject<Monarchy>()
private val boundaryCallback = TimelineBoundaryCallback(paginationRequest, roomId, monarchy, Executors.newSingleThreadExecutor())
private val eventInterceptors = ArrayList<EnrichedEventInterceptor>()

override fun liveTimeline(): LiveData<PagedList<Event>> {
init {
eventInterceptors.add(MessageEventInterceptor(monarchy))
}

override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm ->
ChunkEntity.where(realm, roomId)
.findAll()
@ -33,7 +40,18 @@ data class DefaultRoom(
it.events.where().sort("originServerTs", Sort.DESCENDING)
}
}
val domainSourceFactory = realmDataSourceFactory.map { EventMapper.map(it) }
val domainSourceFactory = realmDataSourceFactory
.map { it.asDomain() }
.map { event ->
val enrichedEvent = EnrichedEvent(event)
eventInterceptors
.filter {
it.canEnrich(enrichedEvent)
}.forEach {
it.enrich(roomId, enrichedEvent)
}
enrichedEvent
}

val pagedListConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
@ -45,5 +63,4 @@ data class DefaultRoom(
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
}


}

View File

@ -8,7 +8,7 @@ import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.getLast
import im.vector.matrix.android.internal.database.query.last
import im.vector.matrix.android.internal.database.query.where
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean
@ -49,10 +49,9 @@ internal class RoomSummaryObserver(private val monarchy: Monarchy) {

private fun manageRoom(roomId: String) {
monarchy.writeAsync { realm ->
val roomEvents = EventEntity.where(realm, roomId).findAll()
val lastNameEvent = roomEvents.getLast(EventType.STATE_ROOM_NAME)?.asDomain()
val lastTopicEvent = roomEvents.getLast(EventType.STATE_ROOM_TOPIC)?.asDomain()
val lastMessageEvent = roomEvents.getLast(EventType.MESSAGE)
val lastNameEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_NAME).last()?.asDomain()
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).last()?.asDomain()
val lastMessageEvent = EventEntity.where(realm, roomId, EventType.MESSAGE).last()

val roomSummary = realm.copyToRealmOrUpdate(RoomSummaryEntity(roomId))
roomSummary.displayName = lastNameEvent?.content<RoomNameContent>()?.name

View File

@ -7,7 +7,6 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.DBConstants
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
@ -20,6 +19,7 @@ import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.session.room.model.PaginationDirection
import im.vector.matrix.android.internal.session.room.model.TokenChunkEvent
import im.vector.matrix.android.internal.session.sync.StateEventsChunkHandler
import im.vector.matrix.android.internal.util.CancelableCoroutine
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
@ -89,19 +89,9 @@ class PaginationRequest(private val roomAPI: RoomAPI,
currentChunk.prevToken = chunkEvent.prevToken


val stateEventsChunk = ChunkEntity.findWithNextToken(realm, roomId, DBConstants.STATE_EVENTS_CHUNK_TOKEN)
?: ChunkEntity().apply {
this.prevToken = DBConstants.STATE_EVENTS_CHUNK_TOKEN
this.nextToken = DBConstants.STATE_EVENTS_CHUNK_TOKEN
}

chunkEvent.stateEvents.forEach { stateEvent ->
val eventEntity = stateEvent.asEntity().let {
realm.copyToRealmOrUpdate(it)
}
if (!stateEventsChunk.events.contains(eventEntity)) {
stateEventsChunk.events.add(0, eventEntity)
}
val stateEventsChunk = StateEventsChunkHandler().handle(realm, roomId, chunkEvent.stateEvents)
if (!roomEntity.chunks.contains(stateEventsChunk)) {
roomEntity.chunks.add(stateEventsChunk)
}

chunkEvent.chunk.forEach { event ->

View File

@ -4,7 +4,7 @@ import android.arch.paging.PagedList
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.session.events.model.Event
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.query.findAllIncludingEvents
import im.vector.matrix.android.internal.session.room.model.PaginationDirection
@ -17,7 +17,7 @@ class TimelineBoundaryCallback(private val paginationRequest: PaginationRequest,
private val roomId: String,
private val monarchy: Monarchy,
ioExecutor: Executor
) : PagedList.BoundaryCallback<Event>() {
) : PagedList.BoundaryCallback<EnrichedEvent>() {

private val helper = PagingRequestHelper(ioExecutor)

@ -25,25 +25,25 @@ class TimelineBoundaryCallback(private val paginationRequest: PaginationRequest,
// actually, it's not possible
}

override fun onItemAtEndLoaded(itemAtEnd: Event) {
override fun onItemAtEndLoaded(itemAtEnd: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.AFTER) {
monarchy.doWithRealm { realm ->
if (itemAtEnd.eventId == null) {
if (itemAtEnd.core.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtEnd.core.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.prevToken, PaginationDirection.BACKWARDS, callback = createCallback(it))
}
}
}

override fun onItemAtFrontLoaded(itemAtFront: Event) {
override fun onItemAtFrontLoaded(itemAtFront: EnrichedEvent) {
helper.runIfNotRunning(PagingRequestHelper.RequestType.BEFORE) {
monarchy.doWithRealm { realm ->
if (itemAtFront.eventId == null) {
if (itemAtFront.core.eventId == null) {
return@doWithRealm
}
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.eventId)).firstOrNull()
val chunkEntity = ChunkEntity.findAllIncludingEvents(realm, Collections.singletonList(itemAtFront.core.eventId)).firstOrNull()
paginationRequest.execute(roomId, chunkEntity?.nextToken, PaginationDirection.FORWARDS, callback = createCallback(it))
}
}

View File

@ -2,7 +2,6 @@ package im.vector.matrix.android.internal.session.sync

import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.DBConstants
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
@ -48,13 +47,13 @@ class RoomSyncHandler(private val monarchy: Monarchy) {

roomEntity.membership = RoomEntity.Membership.JOINED
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val chunkEntity = eventListToChunk(realm, roomId, roomSync.state.events, DBConstants.STATE_EVENTS_CHUNK_TOKEN, DBConstants.STATE_EVENTS_CHUNK_TOKEN)
val chunkEntity = StateEventsChunkHandler().handle(realm, roomId, roomSync.state.events)
if (!roomEntity.chunks.contains(chunkEntity)) {
roomEntity.chunks.add(chunkEntity)
}
}
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {
val chunkEntity = eventListToChunk(realm, roomId, roomSync.timeline.events, roomSync.timeline.prevToken, isLimited = roomSync.timeline.limited)
val chunkEntity = handleListOfEvent(realm, roomId, roomSync.timeline.events, roomSync.timeline.prevToken, isLimited = roomSync.timeline.limited)
if (!roomEntity.chunks.contains(chunkEntity)) {
roomEntity.chunks.add(chunkEntity)
}
@ -70,7 +69,7 @@ class RoomSyncHandler(private val monarchy: Monarchy) {
roomEntity.roomId = roomId
roomEntity.membership = RoomEntity.Membership.INVITED
if (roomSync.inviteState != null && roomSync.inviteState.events.isNotEmpty()) {
val chunkEntity = eventListToChunk(realm, roomId, roomSync.inviteState.events)
val chunkEntity = handleListOfEvent(realm, roomId, roomSync.inviteState.events)
if (!roomEntity.chunks.contains(chunkEntity)) {
roomEntity.chunks.add(chunkEntity)
}
@ -87,12 +86,12 @@ class RoomSyncHandler(private val monarchy: Monarchy) {
}
}

private fun eventListToChunk(realm: Realm,
roomId: String,
eventList: List<Event>,
prevToken: String? = null,
nextToken: String? = null,
isLimited: Boolean = true): ChunkEntity {
private fun handleListOfEvent(realm: Realm,
roomId: String,
eventList: List<Event>,
prevToken: String? = null,
nextToken: String? = null,
isLimited: Boolean = true): ChunkEntity {

val chunkEntity = if (!isLimited) {
ChunkEntity.findLastLiveChunkFromRoom(realm, roomId)

View File

@ -0,0 +1,28 @@
package im.vector.matrix.android.internal.session.sync

import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.internal.database.DBConstants
import im.vector.matrix.android.internal.database.mapper.asEntity
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.query.findWithNextToken
import io.realm.Realm

class StateEventsChunkHandler {

fun handle(realm: Realm, roomId: String, stateEvents: List<Event>): ChunkEntity {
val chunkEntity = ChunkEntity.findWithNextToken(realm, roomId, DBConstants.STATE_EVENTS_CHUNK_TOKEN)
?: ChunkEntity(prevToken = DBConstants.STATE_EVENTS_CHUNK_TOKEN, nextToken = DBConstants.STATE_EVENTS_CHUNK_TOKEN)

stateEvents.forEach { event ->
val eventEntity = event.asEntity().let {
realm.copyToRealmOrUpdate(it)
}
if (!chunkEntity.events.contains(eventEntity)) {
chunkEntity.events.add(eventEntity)
}
}
return chunkEntity
}


}