Timeline : send read-receipt when scrolling. Still need to handle read marker.

This commit is contained in:
ganfra 2019-01-30 18:39:54 +01:00
parent bfaab52b41
commit 6113bba703
17 changed files with 198 additions and 49 deletions

View File

@ -16,10 +16,11 @@


package im.vector.riotredesign.core.epoxy package im.vector.riotredesign.core.epoxy


import android.view.View
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import android.view.View
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty


@ -29,6 +30,7 @@ abstract class KotlinModel(


private var view: View? = null private var view: View? = null
private var onBindCallback: (() -> Unit)? = null private var onBindCallback: (() -> Unit)? = null
private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener<KotlinModel, View>? = null


abstract fun bind() abstract fun bind()


@ -47,6 +49,16 @@ abstract class KotlinModel(
return this return this
} }


override fun onVisibilityStateChanged(visibilityState: Int, view: View) {
onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState)
super.onVisibilityStateChanged(visibilityState, view)
}

fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener<KotlinModel, View>): KotlinModel {
this.onModelVisibilityStateChangedListener = listener
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> {
@ -56,7 +68,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

@ -16,9 +16,12 @@


package im.vector.riotredesign.features.home.room.detail package im.vector.riotredesign.features.home.room.detail


import im.vector.matrix.android.api.session.room.timeline.TimelineEvent

sealed class RoomDetailActions { sealed class RoomDetailActions {


data class SendMessage(val text: String) : RoomDetailActions() data class SendMessage(val text: String) : RoomDetailActions()
object IsDisplayed : RoomDetailActions() object IsDisplayed : RoomDetailActions()
data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions()


} }

View File

@ -23,9 +23,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyVisibilityTracker
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.args import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.fragmentViewModel
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.R 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
@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {


override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
roomDetailViewModel.accept(RoomDetailActions.IsDisplayed) roomDetailViewModel.process(RoomDetailActions.IsDisplayed)
} }


private fun setupToolbar() { private fun setupToolbar() {
@ -86,6 +88,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
} }


private fun setupRecyclerView() { private fun setupRecyclerView() {
val epoxyVisibilityTracker = EpoxyVisibilityTracker()
epoxyVisibilityTracker.attach(recyclerView)
val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true)
scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager)
recyclerView.layoutManager = layoutManager recyclerView.layoutManager = layoutManager
@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
val textMessage = composerEditText.text.toString() val textMessage = composerEditText.text.toString()
if (textMessage.isNotBlank()) { if (textMessage.isNotBlank()) {
composerEditText.text = null composerEditText.text = null
roomDetailViewModel.accept(RoomDetailActions.SendMessage(textMessage)) roomDetailViewModel.process(RoomDetailActions.SendMessage(textMessage))
} }
} }
} }
@ -143,4 +147,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback {
homePermalinkHandler.launch(url) homePermalinkHandler.launch(url)
} }


override fun onEventVisible(event: TimelineEvent, index: Int) {
roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index))
}

} }

View File

@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail


import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.jakewharton.rxrelay2.BehaviorRelay
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -25,7 +26,9 @@ import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.core.platform.RiotViewModel
import im.vector.riotredesign.features.home.room.VisibleRoomHolder import im.vector.riotredesign.features.home.room.VisibleRoomHolder
import io.reactivex.rxkotlin.subscribeBy
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import java.util.concurrent.TimeUnit


class RoomDetailViewModel(initialState: RoomDetailViewState, class RoomDetailViewModel(initialState: RoomDetailViewState,
private val session: Session, private val session: Session,
@ -36,6 +39,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
private val roomId = initialState.roomId private val roomId = initialState.roomId
private val eventId = initialState.eventId private val eventId = initialState.eventId


private val displayedEventsObservable = BehaviorRelay.create<RoomDetailActions.EventDisplayed>()

companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> { companion object : MvRxViewModelFactory<RoomDetailViewModel, RoomDetailViewState> {


@JvmStatic @JvmStatic
@ -49,14 +54,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
init { init {
observeRoomSummary() observeRoomSummary()
observeTimeline() observeTimeline()
observeDisplayedEvents()
room.loadRoomMembersIfNeeded() room.loadRoomMembersIfNeeded()
room.markLatestAsRead(callback = object : MatrixCallback<Void> {})
} }


fun accept(action: RoomDetailActions) { fun process(action: RoomDetailActions) {
when (action) { when (action) {
is RoomDetailActions.SendMessage -> handleSendMessage(action) is RoomDetailActions.SendMessage -> handleSendMessage(action)
is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId) is RoomDetailActions.IsDisplayed -> handleIsDisplayed()
is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action)
} }
} }


@ -66,6 +72,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState,
room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {}) room.sendTextMessage(action.text, callback = object : MatrixCallback<Event> {})
} }


private fun handleEventDisplayed(action: RoomDetailActions.EventDisplayed) {
displayedEventsObservable.accept(action)
}

private fun handleIsDisplayed() {
visibleRoomHolder.setVisibleRoom(roomId)
}

private fun observeDisplayedEvents() {
// We are buffering scroll events for one second
// and keep the most recent one to set the read receipt on.
displayedEventsObservable.hide()
.buffer(1, TimeUnit.SECONDS)
.filter { it.isNotEmpty() }
.subscribeBy { actions ->
val mostRecentEvent = actions.minBy { it.index }
mostRecentEvent?.event?.root?.eventId?.let { eventId ->
room.setReadReceipt(eventId, callback = object : MatrixCallback<Void> {})
}
}
.disposeOnClear()
}

private fun observeRoomSummary() { private fun observeRoomSummary() {
room.rx().liveRoomSummary() room.rx().liveRoomSummary()
.execute { async -> .execute { async ->

View File

@ -16,12 +16,16 @@


package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline


import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyAsyncUtil import com.airbnb.epoxy.EpoxyAsyncUtil
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelVisibilityStateChangedListener
import com.airbnb.epoxy.VisibilityState
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.TimelineData import im.vector.matrix.android.api.session.room.timeline.TimelineData
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.features.home.LoadingItemModel_ import im.vector.riotredesign.features.home.LoadingItemModel_
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
@ -74,6 +78,11 @@ class TimelineEventController(private val roomId: String,


timelineItemFactory.create(event, nextEvent, callback)?.also { timelineItemFactory.create(event, nextEvent, callback)?.also {
it.id(event.localId) it.id(event.localId)
it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener<KotlinModel, View> { model, view, visibilityState ->
if (visibilityState == VisibilityState.VISIBLE) {
callback?.onEventVisible(event, currentPosition)
}
})
epoxyModels.add(it) epoxyModels.add(it)
} }
if (addDaySeparator) { if (addDaySeparator) {
@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String,




interface Callback { interface Callback {
fun onEventVisible(event: TimelineEvent, index: Int)
fun onUrlClicked(url: String) fun onUrlClicked(url: String)
} }



View File

@ -16,9 +16,9 @@


package im.vector.riotredesign.features.home.room.detail.timeline package im.vector.riotredesign.features.home.room.detail.timeline


import com.airbnb.epoxy.EpoxyModel
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotredesign.core.epoxy.KotlinModel


class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,
private val roomNameItemFactory: RoomNameItemFactory, private val roomNameItemFactory: RoomNameItemFactory,
@ -28,14 +28,14 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory,


fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
callback: TimelineEventController.Callback?): EpoxyModel<*>? { callback: TimelineEventController.Callback?): KotlinModel? {


return when (event.root.type) { return when (event.root.type) {
EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback)
EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event)
EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event)
EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event)
else -> defaultItemFactory.create(event) else -> defaultItemFactory.create(event)
} }
} }



View File

@ -18,14 +18,24 @@ package im.vector.matrix.android.api.session.room.read


import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback


/**
* This interface defines methods to handle read receipts and read marker in a room. It's implemented at the room level.
*/
interface ReadService { interface ReadService {


fun markLatestAsRead(callback: MatrixCallback<Void>) /**

* Force the read marker to be set on the latest event.
*/
fun markAllAsRead(callback: MatrixCallback<Void>) fun markAllAsRead(callback: MatrixCallback<Void>)


/**
* Set the read receipt on the event with provided eventId.
*/
fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>) fun setReadReceipt(eventId: String, callback: MatrixCallback<Void>)


fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback<Void>) /**
* Set the read marker on the event with provided eventId.
*/
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>)


} }

View File

@ -18,11 +18,11 @@ package im.vector.matrix.android.internal.di


import im.vector.matrix.android.internal.network.AccessTokenInterceptor import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.network.UnitConverterFactory
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import okreplay.OkReplayInterceptor import okreplay.OkReplayInterceptor
import org.koin.dsl.module.module import org.koin.dsl.module.module
import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.converter.moshi.MoshiConverterFactory
import timber.log.Timber import timber.log.Timber
@ -62,10 +62,6 @@ class NetworkModule {
MoshiProvider.providesMoshi() MoshiProvider.providesMoshi()
} }


single {
MoshiConverterFactory.create(get()) as Converter.Factory
}

single { single {
NetworkConnectivityChecker(get()) NetworkConnectivityChecker(get())
} }
@ -73,7 +69,8 @@ class NetworkModule {
factory { factory {
Retrofit.Builder() Retrofit.Builder()
.client(get()) .client(get())
.addConverterFactory(get()) .addConverterFactory(UnitConverterFactory)
.addConverterFactory(MoshiConverterFactory.create(get()))
} }


} }

View File

@ -0,0 +1,35 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.matrix.android.internal.network

import okhttp3.ResponseBody
import retrofit2.Converter
import retrofit2.Retrofit
import java.lang.reflect.Type

object UnitConverterFactory : Converter.Factory() {
override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>,
retrofit: Retrofit): Converter<ResponseBody, *>? {
return if (type == Unit::class.java) UnitConverter else null
}

private object UnitConverter : Converter<ResponseBody, Unit> {
override fun convert(value: ResponseBody) {
value.close()
}
}
}

View File

@ -47,6 +47,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
sessionParams sessionParams
} }


scope(DefaultSession.SCOPE) {
sessionParams.credentials
}

scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val context = get<Context>() val context = get<Context>()
val childPath = sessionParams.credentials.userId.md5() val childPath = sessionParams.credentials.userId.md5()

View File

@ -26,13 +26,13 @@ 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.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchManaged import im.vector.matrix.android.internal.util.fetchCopied


internal class DefaultRoomService(private val monarchy: Monarchy, internal class DefaultRoomService(private val monarchy: Monarchy,
private val roomFactory: RoomFactory) : RoomService { private val roomFactory: RoomFactory) : RoomService {


override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
monarchy.fetchManaged { RoomEntity.where(it, roomId).findFirst() } ?: return null monarchy.fetchCopied { RoomEntity.where(it, roomId).findFirst() } ?: return null
return roomFactory.instantiate(roomId) return roomFactory.instantiate(roomId)
} }



View File

@ -112,7 +112,7 @@ internal interface RoomAPI {
* @param markers the read markers * @param markers the read markers
*/ */
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers")
fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map<String, String>): Call<Void> fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map<String, String>): Call<Unit>




} }

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor
@ -34,6 +35,7 @@ import java.util.concurrent.Executors


internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask, internal class RoomFactory(private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val credentials: Credentials,
private val paginationTask: PaginationTask, private val paginationTask: PaginationTask,
private val contextOfEventTask: GetContextOfEventTask, private val contextOfEventTask: GetContextOfEventTask,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,

View File

@ -16,7 +16,6 @@


package im.vector.matrix.android.internal.session.room package im.vector.matrix.android.internal.session.room


import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.session.DefaultSession import im.vector.matrix.android.internal.session.DefaultSession
import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.DefaultLoadRoomMembersTask
import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask
@ -58,16 +57,15 @@ class RoomModule {
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
DefaultSetReadMarkersTask(get()) as SetReadMarkersTask DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
val sessionParams = get<SessionParams>() EventFactory(get())
EventFactory(sessionParams.credentials)
} }


scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
RoomFactory(get(), get(), get(), get(), get(), get(), get()) RoomFactory(get(), get(), get(), get(), get(), get(), get(), get())
} }


} }

View File

@ -23,22 +23,16 @@ import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.latestEvent import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.util.fetchManaged import im.vector.matrix.android.internal.util.fetchCopied


internal class DefaultReadService(private val roomId: String, internal class DefaultReadService(private val roomId: String,
private val monarchy: Monarchy, private val monarchy: Monarchy,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,
private val taskExecutor: TaskExecutor) : ReadService { private val taskExecutor: TaskExecutor) : ReadService {


override fun markLatestAsRead(callback: MatrixCallback<Void>) {
val lastEvent = getLatestEvent()
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = lastEvent?.eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
}

override fun markAllAsRead(callback: MatrixCallback<Void>) { override fun markAllAsRead(callback: MatrixCallback<Void>) {
val lastEvent = getLatestEvent() val latestEvent = getLatestEvent()
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = lastEvent?.eventId, readReceiptEventId = null) val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor) setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
} }


@ -47,13 +41,14 @@ internal class DefaultReadService(private val roomId: String,
setReadMarkersTask.configureWith(params).executeBy(taskExecutor) setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
} }


override fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback<Void>) { override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Void>) {
val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = readReceiptEventId) val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null)
setReadMarkersTask.configureWith(params).executeBy(taskExecutor) setReadMarkersTask.configureWith(params).executeBy(taskExecutor)
} }


private fun getLatestEvent(): EventEntity? { private fun getLatestEvent(): EventEntity? {
return monarchy.fetchManaged { EventEntity.latestEvent(it, roomId) } return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) }
} }



} }

View File

@ -17,11 +17,22 @@
package im.vector.matrix.android.internal.session.room.read package im.vector.matrix.android.internal.session.room.read


import arrow.core.Try import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.Credentials
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.ReadReceiptEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.find
import im.vector.matrix.android.internal.database.query.findLastLiveChunkFromRoom
import im.vector.matrix.android.internal.database.query.latestEvent
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.network.executeRequest 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.RoomAPI
import im.vector.matrix.android.internal.task.Task import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionAsync


internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Void> { internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Unit> {


data class Params( data class Params(
val roomId: String, val roomId: String,
@ -33,19 +44,54 @@ internal interface SetReadMarkersTask : Task<SetReadMarkersTask.Params, Void> {
private const val READ_MARKER = "m.fully_read" private const val READ_MARKER = "m.fully_read"
private const val READ_RECEIPT = "m.read" private const val READ_RECEIPT = "m.read"


internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI,
private val credentials: Credentials,
private val monarchy: Monarchy
) : SetReadMarkersTask { ) : SetReadMarkersTask {


override fun execute(params: SetReadMarkersTask.Params): Try<Void> { override fun execute(params: SetReadMarkersTask.Params): Try<Unit> {
val markers = HashMap<String, String>() val markers = HashMap<String, String>()
if (params.fullyReadEventId?.isNotEmpty() == true) { if (params.fullyReadEventId?.isNotEmpty() == true) {
markers[READ_MARKER] = params.fullyReadEventId markers[READ_MARKER] = params.fullyReadEventId
} }
if (params.readReceiptEventId?.isNotEmpty() == true) { if (params.readReceiptEventId?.isNotEmpty() == true && !isEventRead(params.roomId, params.readReceiptEventId)) {
updateNotificationCountIfNecessary(params.roomId, params.readReceiptEventId)
markers[READ_RECEIPT] = params.readReceiptEventId markers[READ_RECEIPT] = params.readReceiptEventId
} }
return executeRequest { return if (markers.isEmpty()) {
apiCall = roomAPI.sendReadMarker(params.roomId, markers) Try.just(Unit)
} else {
executeRequest {
apiCall = roomAPI.sendReadMarker(params.roomId, markers)
}
} }
} }

private fun updateNotificationCountIfNecessary(roomId: String, eventId: String) {
monarchy.tryTransactionAsync { realm ->
val isLatestReceived = EventEntity.latestEvent(realm, eventId)?.eventId == eventId
if (isLatestReceived) {
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@tryTransactionAsync
roomSummary.notificationCount = 0
roomSummary.highlightCount = 0
}
}
}

private fun isEventRead(roomId: String, eventId: String): Boolean {
var isEventRead = false
monarchy.doWithRealm {
val readReceipt = ReadReceiptEntity.where(it, roomId, credentials.userId).findFirst()
?: return@doWithRealm
val liveChunk = ChunkEntity.findLastLiveChunkFromRoom(it, roomId)
?: return@doWithRealm
val readReceiptIndex = liveChunk.events.find(readReceipt.eventId)?.displayIndex
?: -1
val eventToCheckIndex = liveChunk.events.find(eventId)?.displayIndex ?: -1
isEventRead = eventToCheckIndex >= readReceiptIndex
}
return isEventRead
}

} }

View File

@ -34,10 +34,10 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit):
} }
} }


fun <T : RealmModel> Monarchy.fetchManaged(query: (Realm) -> T?): T? { fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
val ref = AtomicReference<T>() val ref = AtomicReference<T>()
doWithRealm { realm -> doWithRealm { realm ->
val result = query.invoke(realm) val result = query.invoke(realm)?.let { realm.copyFromRealm(it) }
ref.set(result) ref.set(result)
} }
return ref.get() return ref.get()