Start introducing a way to open timeline around an event

This commit is contained in:
ganfra 2018-11-19 15:47:54 +01:00
parent 40fa326771
commit d250d2bd27
10 changed files with 260 additions and 27 deletions

View file

@ -6,21 +6,22 @@ import android.support.v4.app.BundleCompat
import android.support.v4.app.Fragment import android.support.v4.app.Fragment
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
class FragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fragment, T> { class FragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fragment, T?> {
var value: T? = null var value: T? = null
override operator fun getValue(thisRef: android.support.v4.app.Fragment, property: kotlin.reflect.KProperty<*>): T { override operator fun getValue(thisRef: android.support.v4.app.Fragment, property: kotlin.reflect.KProperty<*>): T? {
if (value == null) { if (value == null) {
val args = thisRef.arguments val args = thisRef.arguments
?: throw IllegalStateException("Cannot read property ${property.name} if no arguments have been set")
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
value = args.get(property.name) as T value = args?.get(property.name) as T?
} }
return value ?: throw IllegalStateException("Property ${property.name} could not be read") return value
} }
override operator fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) { override operator fun setValue(thisRef: Fragment, property: KProperty<*>, value: T?) {
if (value == null) return
if (thisRef.arguments == null) { if (thisRef.arguments == null) {
thisRef.arguments = Bundle() thisRef.arguments = Bundle()
} }
@ -42,7 +43,21 @@ class FragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fr
is Binder -> BundleCompat.putBinder(args, key, value) is Binder -> BundleCompat.putBinder(args, key, value)
is android.os.Parcelable -> args.putParcelable(key, value) is android.os.Parcelable -> args.putParcelable(key, value)
is java.io.Serializable -> args.putSerializable(key, value) is java.io.Serializable -> args.putSerializable(key, value)
else -> throw IllegalStateException("Type ${value.javaClass.canonicalName} of property ${property.name} is not supported") else -> throw IllegalStateException("Type ${value.javaClass.name} of property ${property.name} is not supported")
} }
} }
} }
class UnsafeFragmentArgumentDelegate<T : Any> : kotlin.properties.ReadWriteProperty<Fragment, T> {
private val innerDelegate = FragmentArgumentDelegate<T>()
override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
innerDelegate.setValue(thisRef, property, value)
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
return innerDelegate.getValue(thisRef, property)!!
}
}

View file

@ -15,6 +15,7 @@ import im.vector.riotredesign.R
import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.RiotFragment
import im.vector.riotredesign.core.platform.ToolbarConfigurable import im.vector.riotredesign.core.platform.ToolbarConfigurable
import im.vector.riotredesign.core.utils.FragmentArgumentDelegate import im.vector.riotredesign.core.utils.FragmentArgumentDelegate
import im.vector.riotredesign.core.utils.UnsafeFragmentArgumentDelegate
import im.vector.riotredesign.features.home.AvatarRenderer import im.vector.riotredesign.features.home.AvatarRenderer
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
@ -25,16 +26,18 @@ class RoomDetailFragment : RiotFragment() {
companion object { companion object {
fun newInstance(roomId: String): RoomDetailFragment { fun newInstance(roomId: String, eventId: String? = null): RoomDetailFragment {
return RoomDetailFragment().apply { return RoomDetailFragment().apply {
this.roomId = roomId this.roomId = roomId
this.eventId = eventId
} }
} }
} }
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: String by UnsafeFragmentArgumentDelegate()
private var eventId: String? by FragmentArgumentDelegate()
private val timelineEventController by inject<TimelineEventController>(parameters = { ParameterList(roomId) }) private val timelineEventController by inject<TimelineEventController>(parameters = { ParameterList(roomId) })
private lateinit var room: Room private lateinit var room: Room
@ -48,7 +51,7 @@ class RoomDetailFragment : RiotFragment() {
setupRecyclerView() setupRecyclerView()
setupToolbar() setupToolbar()
room.loadRoomMembersIfNeeded() room.loadRoomMembersIfNeeded()
room.liveTimeline().observe(this, Observer { renderEvents(it) }) room.timeline(eventId).observe(this, Observer { renderEvents(it) })
room.roomSummary.observe(this, Observer { renderRoomSummary(it) }) room.roomSummary.observe(this, Observer { renderRoomSummary(it) })
sendButton.setOnClickListener { sendButton.setOnClickListener {
val textMessage = composerEditText.text.toString() val textMessage = composerEditText.text.toString()

View file

@ -6,6 +6,6 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent
interface TimelineHolder { interface TimelineHolder {
fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> fun timeline(eventId: String? = null): LiveData<PagedList<EnrichedEvent>>
} }

View file

@ -12,8 +12,10 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti
internal fun ChunkEntity.merge(chunkEntity: ChunkEntity, internal fun ChunkEntity.merge(chunkEntity: ChunkEntity,
direction: PaginationDirection) { direction: PaginationDirection) {
val events = chunkEntity.events.map { it.asDomain() }
addAll(events, direction) chunkEntity.events.forEach {
addOrUpdate(it.asDomain(), direction)
}
if (direction == PaginationDirection.FORWARDS) { if (direction == PaginationDirection.FORWARDS) {
nextToken = chunkEntity.nextToken nextToken = chunkEntity.nextToken
} else { } else {
@ -26,15 +28,13 @@ internal fun ChunkEntity.addAll(events: List<Event>,
updateStateIndex: Boolean = true) { updateStateIndex: Boolean = true) {
events.forEach { event -> events.forEach { event ->
if (updateStateIndex && event.isStateEvent()) { addOrUpdate(event, direction, updateStateIndex)
updateStateIndex(direction)
}
addOrUpdate(event, direction)
} }
} }
internal fun ChunkEntity.addOrUpdate(event: Event, internal fun ChunkEntity.addOrUpdate(event: Event,
direction: PaginationDirection) { direction: PaginationDirection,
updateStateIndex: Boolean = true) {
if (!isManaged) { if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains") throw IllegalStateException("Chunk entity should be managed to use fast contains")
} }
@ -43,6 +43,10 @@ internal fun ChunkEntity.addOrUpdate(event: Event,
return return
} }
if (updateStateIndex && event.isStateEvent()) {
updateStateIndex(direction)
}
val currentStateIndex = stateIndex(direction) val currentStateIndex = stateIndex(direction)
if (!events.fastContains(event.eventId)) { if (!events.fastContains(event.eventId)) {
val eventEntity = event.asEntity() val eventEntity = event.asEntity()

View file

@ -46,8 +46,8 @@ internal data class DefaultRoom(
} }
} }
override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> { override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
return timelineHolder.liveTimeline() return timelineHolder.timeline(eventId)
} }
override fun loadRoomMembersIfNeeded(): Cancelable { override fun loadRoomMembersIfNeeded(): Cancelable {

View file

@ -1,9 +1,11 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.room.model.MessageContent import im.vector.matrix.android.api.session.room.model.MessageContent
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse import im.vector.matrix.android.internal.session.room.members.RoomMembersResponse
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.session.room.timeline.EventContextResponse
import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body import retrofit2.http.Body
@ -63,5 +65,28 @@ internal interface RoomAPI {
@Body content: MessageContent @Body content: MessageContent
): Call<SendResponse> ): Call<SendResponse>
/**
* Get the context surrounding an event.
*
* @param roomId the room id
* @param eventId the event Id
* @param limit the maximum number of messages to retrieve
* @param filter A JSON RoomEventFilter to filter returned events with. Optional.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/context/{eventId}")
fun getContextOfEvent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@Query("limit") limit: Int,
@Query("filter") filter: String? = null): Call<EventContextResponse>
/**
* Retrieve an event from its room id / events id
*
* @param roomId the room id
* @param eventId the event Id
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}")
fun getEvent(@Path("roomId") roomId: String, @Path("eventId") eventId: String): Call<Event>
} }

View file

@ -13,6 +13,8 @@ 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.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.MessageEventInterceptor import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor
import io.realm.Realm
import io.realm.RealmQuery
private const val PAGE_SIZE = 30 private const val PAGE_SIZE = 30
@ -28,12 +30,12 @@ internal class DefaultTimelineHolder(private val roomId: String,
eventInterceptors.add(MessageEventInterceptor(monarchy, roomId)) eventInterceptors.add(MessageEventInterceptor(monarchy, roomId))
} }
override fun liveTimeline(): LiveData<PagedList<EnrichedEvent>> { override fun timeline(eventId: String?): LiveData<PagedList<EnrichedEvent>> {
val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> if (eventId != null) {
EventEntity fetchEventIfNeeded()
.where(realm, roomId = roomId) }
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) val realmDataSourceFactory = monarchy.createDataSourceFactory {
.sort(EventEntityFields.DISPLAY_INDEX) buildDataSourceFactoryQuery(it, eventId)
} }
val domainSourceFactory = realmDataSourceFactory val domainSourceFactory = realmDataSourceFactory
.map { it.asDomain() } .map { it.asDomain() }
@ -59,4 +61,23 @@ internal class DefaultTimelineHolder(private val roomId: String,
val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback) val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback)
return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder)
} }
private fun fetchEventIfNeeded() {
}
private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery<EventEntity> {
val query = if (eventId == null) {
EventEntity
.where(realm, roomId = roomId)
.equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true)
} else {
EventEntity
.where(realm, roomId = roomId)
.`in`("${EventEntityFields.CHUNK}.${ChunkEntityFields.EVENTS.EVENT_ID}", arrayOf(eventId))
}
return query.sort(EventEntityFields.DISPLAY_INDEX)
}
} }

View file

@ -0,0 +1,22 @@
package im.vector.matrix.android.internal.session.room.timeline
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import im.vector.matrix.android.api.session.events.model.Event
@JsonClass(generateAdapter = true)
data class EventContextResponse(
@Json(name = "event") val event: Event,
@Json(name = "start") val prevToken: String? = null,
@Json(name = "events_before") val eventsBefore: List<Event> = emptyList(),
@Json(name = "events_after") val eventsAfter: List<Event> = emptyList(),
@Json(name = "end") val nextToken: String? = null,
@Json(name = "state") val stateEvents: List<Event> = emptyList()
) {
val timelineEvents: List<Event> by lazy {
eventsBefore + event + eventsAfter
}
}

View file

@ -0,0 +1,102 @@
package im.vector.matrix.android.internal.session.room.timeline
import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.database.helper.addAll
import im.vector.matrix.android.internal.database.helper.addOrUpdate
import im.vector.matrix.android.internal.database.helper.deleteOnCascade
import im.vector.matrix.android.internal.database.helper.merge
import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.legacy.util.FilterUtil
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
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 im.vector.matrix.android.internal.util.tryTransactionSync
import io.realm.kotlin.createObject
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class GetContextOfEventRequest(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val stateEventsChunkHandler: StateEventsChunkHandler
) {
fun execute(roomId: String,
eventId: String,
callback: MatrixCallback<EventContextResponse>
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val filter = FilterUtil.createRoomEventFilter(true)?.toJSONString()
val contextOrFailure = execute(roomId, eventId, filter)
contextOrFailure.fold({ callback.onFailure(it) }, { callback.onSuccess(it) })
}
return CancelableCoroutine(job)
}
private suspend fun execute(roomId: String,
eventId: String,
filter: String?) = withContext(coroutineDispatchers.io) {
executeRequest<EventContextResponse> {
apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter)
}.flatMap { response ->
insertInDb(response, roomId)
}
}
private fun insertInDb(response: EventContextResponse, roomId: String): Try<EventContextResponse> {
return monarchy
.tryTransactionSync { realm ->
val roomEntity = RoomEntity.where(realm, roomId).findFirst()
?: throw IllegalStateException("You shouldn't use this method without a room")
val currentChunk = realm.createObject<ChunkEntity>().apply {
prevToken = response.prevToken
nextToken = response.nextToken
}
currentChunk.addOrUpdate(response.event, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsAfter, PaginationDirection.FORWARDS)
currentChunk.addAll(response.eventsBefore, PaginationDirection.BACKWARDS)
// Now, handles chunk merge
val prevChunk = ChunkEntity.find(realm, roomId, nextToken = response.prevToken)
val nextChunk = ChunkEntity.find(realm, roomId, prevToken = response.nextToken)
if (prevChunk != null) {
currentChunk.merge(prevChunk, PaginationDirection.BACKWARDS)
roomEntity.deleteOnCascade(prevChunk)
}
if (nextChunk != null) {
currentChunk.merge(nextChunk, PaginationDirection.FORWARDS)
roomEntity.deleteOnCascade(nextChunk)
}
/*
val eventIds = response.timelineEvents.mapNotNull { it.eventId }
ChunkEntity
.findAllIncludingEvents(realm, eventIds)
.filter { it != currentChunk }
.forEach { overlapped ->
currentChunk.merge(overlapped, direction)
roomEntity.deleteOnCascade(overlapped)
}
*/
roomEntity.addOrUpdate(currentChunk)
val stateEventsChunk = stateEventsChunkHandler.handle(realm, roomId, response.stateEvents)
roomEntity.addOrUpdate(stateEventsChunk)
}
.map { response }
}
}

View file

@ -0,0 +1,41 @@
package im.vector.matrix.android.internal.session.room.timeline
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
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
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
internal class GetEventRequest(private val roomAPI: RoomAPI,
private val monarchy: Monarchy,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val stateEventsChunkHandler: StateEventsChunkHandler
) {
fun execute(roomId: String,
eventId: String,
callback: MatrixCallback<Event>
): Cancelable {
val job = GlobalScope.launch(coroutineDispatchers.main) {
val eventOrFailure = execute(roomId, eventId)
eventOrFailure.fold({ callback.onFailure(it) }, { callback.onSuccess(it) })
}
return CancelableCoroutine(job)
}
private suspend fun execute(roomId: String,
eventId: String) = withContext(coroutineDispatchers.io) {
executeRequest<Event> {
apiCall = roomAPI.getEvent(roomId, eventId)
}
}
}