From d250d2bd277e1c67501f1d1e4fefe4d787d3fe2d Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 19 Nov 2018 15:47:54 +0100 Subject: [PATCH] Start introducing a way to open timeline around an event --- .../core/utils/FragmentArgumentDelegate.kt | 31 ++++-- .../home/room/detail/RoomDetailFragment.kt | 9 +- .../api/session/room/TimelineHolder.kt | 2 +- .../database/helper/ChunkEntityHelper.kt | 18 ++-- .../internal/session/room/DefaultRoom.kt | 4 +- .../android/internal/session/room/RoomAPI.kt | 25 +++++ .../room/timeline/DefaultTimelineHolder.kt | 33 ++++-- .../room/timeline/EventContextResponse.kt | 22 ++++ .../room/timeline/GetContextOfEventRequest.kt | 102 ++++++++++++++++++ .../session/room/timeline/GetEventRequest.kt | 41 +++++++ 10 files changed, 260 insertions(+), 27 deletions(-) create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt create mode 100644 matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventRequest.kt diff --git a/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt b/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt index 28a5419817..565c421cae 100644 --- a/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt +++ b/app/src/main/java/im/vector/riotredesign/core/utils/FragmentArgumentDelegate.kt @@ -6,21 +6,22 @@ import android.support.v4.app.BundleCompat import android.support.v4.app.Fragment import kotlin.reflect.KProperty -class FragmentArgumentDelegate : kotlin.properties.ReadWriteProperty { +class FragmentArgumentDelegate : kotlin.properties.ReadWriteProperty { 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) { val args = thisRef.arguments - ?: throw IllegalStateException("Cannot read property ${property.name} if no arguments have been set") @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) { thisRef.arguments = Bundle() } @@ -42,7 +43,21 @@ class FragmentArgumentDelegate : kotlin.properties.ReadWriteProperty BundleCompat.putBinder(args, key, value) is android.os.Parcelable -> args.putParcelable(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") } } -} \ No newline at end of file +} + +class UnsafeFragmentArgumentDelegate : kotlin.properties.ReadWriteProperty { + + private val innerDelegate = FragmentArgumentDelegate() + + 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)!! + } + +} \ 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 b921100379..4775bb0cbc 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 @@ -15,6 +15,7 @@ import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.ToolbarConfigurable 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.room.detail.timeline.TimelineEventController import kotlinx.android.synthetic.main.fragment_room_detail.* @@ -25,16 +26,18 @@ class RoomDetailFragment : RiotFragment() { companion object { - fun newInstance(roomId: String): RoomDetailFragment { + fun newInstance(roomId: String, eventId: String? = null): RoomDetailFragment { return RoomDetailFragment().apply { this.roomId = roomId + this.eventId = eventId } } } private val matrix by inject() private val currentSession = matrix.currentSession - private var roomId by FragmentArgumentDelegate() + private var roomId: String by UnsafeFragmentArgumentDelegate() + private var eventId: String? by FragmentArgumentDelegate() private val timelineEventController by inject(parameters = { ParameterList(roomId) }) private lateinit var room: Room @@ -48,7 +51,7 @@ class RoomDetailFragment : RiotFragment() { setupRecyclerView() setupToolbar() room.loadRoomMembersIfNeeded() - room.liveTimeline().observe(this, Observer { renderEvents(it) }) + room.timeline(eventId).observe(this, Observer { renderEvents(it) }) room.roomSummary.observe(this, Observer { renderRoomSummary(it) }) sendButton.setOnClickListener { val textMessage = composerEditText.text.toString() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt index 535f681a71..fc01a47b59 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/TimelineHolder.kt @@ -6,6 +6,6 @@ import im.vector.matrix.android.api.session.events.model.EnrichedEvent interface TimelineHolder { - fun liveTimeline(): LiveData> + fun timeline(eventId: String? = null): LiveData> } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt index a9210550bb..07316c10f1 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/helper/ChunkEntityHelper.kt @@ -12,8 +12,10 @@ import im.vector.matrix.android.internal.session.room.timeline.PaginationDirecti internal fun ChunkEntity.merge(chunkEntity: ChunkEntity, direction: PaginationDirection) { - val events = chunkEntity.events.map { it.asDomain() } - addAll(events, direction) + + chunkEntity.events.forEach { + addOrUpdate(it.asDomain(), direction) + } if (direction == PaginationDirection.FORWARDS) { nextToken = chunkEntity.nextToken } else { @@ -26,15 +28,13 @@ internal fun ChunkEntity.addAll(events: List, updateStateIndex: Boolean = true) { events.forEach { event -> - if (updateStateIndex && event.isStateEvent()) { - updateStateIndex(direction) - } - addOrUpdate(event, direction) + addOrUpdate(event, direction, updateStateIndex) } } internal fun ChunkEntity.addOrUpdate(event: Event, - direction: PaginationDirection) { + direction: PaginationDirection, + updateStateIndex: Boolean = true) { if (!isManaged) { throw IllegalStateException("Chunk entity should be managed to use fast contains") } @@ -43,6 +43,10 @@ internal fun ChunkEntity.addOrUpdate(event: Event, return } + if (updateStateIndex && event.isStateEvent()) { + updateStateIndex(direction) + } + val currentStateIndex = stateIndex(direction) if (!events.fastContains(event.eventId)) { val eventEntity = event.asEntity() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt index 1388c14140..07b8a9fe5d 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoom.kt @@ -46,8 +46,8 @@ internal data class DefaultRoom( } } - override fun liveTimeline(): LiveData> { - return timelineHolder.liveTimeline() + override fun timeline(eventId: String?): LiveData> { + return timelineHolder.timeline(eventId) } override fun loadRoomMembersIfNeeded(): Cancelable { diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt index 655cd33d1a..31311bcdab 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomAPI.kt @@ -1,9 +1,11 @@ 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.internal.network.NetworkConstants 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.timeline.EventContextResponse import im.vector.matrix.android.internal.session.room.timeline.TokenChunkEvent import retrofit2.Call import retrofit2.http.Body @@ -63,5 +65,28 @@ internal interface RoomAPI { @Body content: MessageContent ): Call + /** + * 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 + + /** + * 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 + } \ No newline at end of file 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 21095bcd39..ca7299db4c 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 @@ -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.query.where import im.vector.matrix.android.internal.session.events.interceptor.MessageEventInterceptor +import io.realm.Realm +import io.realm.RealmQuery private const val PAGE_SIZE = 30 @@ -28,12 +30,12 @@ internal class DefaultTimelineHolder(private val roomId: String, eventInterceptors.add(MessageEventInterceptor(monarchy, roomId)) } - override fun liveTimeline(): LiveData> { - val realmDataSourceFactory = monarchy.createDataSourceFactory { realm -> - EventEntity - .where(realm, roomId = roomId) - .equalTo("${EventEntityFields.CHUNK}.${ChunkEntityFields.IS_LAST}", true) - .sort(EventEntityFields.DISPLAY_INDEX) + override fun timeline(eventId: String?): LiveData> { + if (eventId != null) { + fetchEventIfNeeded() + } + val realmDataSourceFactory = monarchy.createDataSourceFactory { + buildDataSourceFactoryQuery(it, eventId) } val domainSourceFactory = realmDataSourceFactory .map { it.asDomain() } @@ -59,4 +61,23 @@ internal class DefaultTimelineHolder(private val roomId: String, val livePagedListBuilder = LivePagedListBuilder(domainSourceFactory, pagedListConfig).setBoundaryCallback(boundaryCallback) return monarchy.findAllPagedWithChanges(realmDataSourceFactory, livePagedListBuilder) } + + private fun fetchEventIfNeeded() { + + } + + private fun buildDataSourceFactoryQuery(realm: Realm, eventId: String?): RealmQuery { + 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) + } + + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt new file mode 100644 index 0000000000..79cfac72c9 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/EventContextResponse.kt @@ -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 = emptyList(), + @Json(name = "events_after") val eventsAfter: List = emptyList(), + @Json(name = "end") val nextToken: String? = null, + @Json(name = "state") val stateEvents: List = emptyList() +) { + + val timelineEvents: List by lazy { + eventsBefore + event + eventsAfter + } + + +} diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt new file mode 100644 index 0000000000..fcab9e8297 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetContextOfEventRequest.kt @@ -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 + ): 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 { + apiCall = roomAPI.getContextOfEvent(roomId, eventId, 1, filter) + }.flatMap { response -> + insertInDb(response, roomId) + } + } + + private fun insertInDb(response: EventContextResponse, roomId: String): Try { + 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().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 } + } + + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventRequest.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventRequest.kt new file mode 100644 index 0000000000..1a8ebc7b3b --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/timeline/GetEventRequest.kt @@ -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 + ): 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 { + apiCall = roomAPI.getEvent(roomId, eventId) + } + } + +} \ No newline at end of file