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 ef4dd32b..dd651468 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 @@ -16,10 +16,11 @@ package im.vector.riotredesign.core.epoxy +import android.view.View import androidx.annotation.IdRes import androidx.annotation.LayoutRes -import android.view.View import com.airbnb.epoxy.EpoxyModel +import com.airbnb.epoxy.OnModelVisibilityStateChangedListener import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -29,6 +30,7 @@ abstract class KotlinModel( private var view: View? = null private var onBindCallback: (() -> Unit)? = null + private var onModelVisibilityStateChangedListener: OnModelVisibilityStateChangedListener? = null abstract fun bind() @@ -47,6 +49,16 @@ abstract class KotlinModel( return this } + override fun onVisibilityStateChanged(visibilityState: Int, view: View) { + onModelVisibilityStateChangedListener?.onVisibilityStateChanged(this, view, visibilityState) + super.onVisibilityStateChanged(visibilityState, view) + } + + fun setOnVisibilityStateChanged(listener: OnModelVisibilityStateChangedListener): KotlinModel { + this.onModelVisibilityStateChangedListener = listener + return this + } + override fun getDefaultLayout() = layoutRes protected fun bind(@IdRes id: Int) = object : ReadOnlyProperty { @@ -56,7 +68,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/RoomDetailActions.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt index 7946e89c..f3ca3b3d 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailActions.kt @@ -16,9 +16,12 @@ package im.vector.riotredesign.features.home.room.detail +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent + sealed class RoomDetailActions { data class SendMessage(val text: String) : RoomDetailActions() object IsDisplayed : RoomDetailActions() + data class EventDisplayed(val event: TimelineEvent, val index: Int) : RoomDetailActions() } \ 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 80d56cbf..76f91c7b 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 @@ -23,9 +23,11 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.airbnb.epoxy.EpoxyVisibilityTracker import com.airbnb.mvrx.Success import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel +import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.riotredesign.R import im.vector.riotredesign.core.platform.RiotFragment import im.vector.riotredesign.core.platform.ToolbarConfigurable @@ -75,7 +77,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { override fun onResume() { super.onResume() - roomDetailViewModel.accept(RoomDetailActions.IsDisplayed) + roomDetailViewModel.process(RoomDetailActions.IsDisplayed) } private fun setupToolbar() { @@ -86,6 +88,8 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { } private fun setupRecyclerView() { + val epoxyVisibilityTracker = EpoxyVisibilityTracker() + epoxyVisibilityTracker.attach(recyclerView) val layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, true) scrollOnNewMessageCallback = ScrollOnNewMessageCallback(layoutManager) recyclerView.layoutManager = layoutManager @@ -100,7 +104,7 @@ class RoomDetailFragment : RiotFragment(), TimelineEventController.Callback { val textMessage = composerEditText.text.toString() if (textMessage.isNotBlank()) { 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) } + override fun onEventVisible(event: TimelineEvent, index: Int) { + roomDetailViewModel.process(RoomDetailActions.EventDisplayed(event, index)) + } + } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt index 57ca3508..abdf1a1c 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/RoomDetailViewModel.kt @@ -18,6 +18,7 @@ package im.vector.riotredesign.features.home.room.detail import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext +import com.jakewharton.rxrelay2.BehaviorRelay import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.MatrixCallback 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.riotredesign.core.platform.RiotViewModel import im.vector.riotredesign.features.home.room.VisibleRoomHolder +import io.reactivex.rxkotlin.subscribeBy import org.koin.android.ext.android.get +import java.util.concurrent.TimeUnit class RoomDetailViewModel(initialState: RoomDetailViewState, private val session: Session, @@ -36,6 +39,8 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, private val roomId = initialState.roomId private val eventId = initialState.eventId + private val displayedEventsObservable = BehaviorRelay.create() + companion object : MvRxViewModelFactory { @JvmStatic @@ -49,14 +54,15 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, init { observeRoomSummary() observeTimeline() + observeDisplayedEvents() room.loadRoomMembersIfNeeded() - room.markLatestAsRead(callback = object : MatrixCallback {}) } - fun accept(action: RoomDetailActions) { + fun process(action: RoomDetailActions) { when (action) { - is RoomDetailActions.SendMessage -> handleSendMessage(action) - is RoomDetailActions.IsDisplayed -> visibleRoomHolder.setVisibleRoom(roomId) + is RoomDetailActions.SendMessage -> handleSendMessage(action) + is RoomDetailActions.IsDisplayed -> handleIsDisplayed() + is RoomDetailActions.EventDisplayed -> handleEventDisplayed(action) } } @@ -66,6 +72,29 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, room.sendTextMessage(action.text, callback = object : MatrixCallback {}) } + 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 {}) + } + } + .disposeOnClear() + } + private fun observeRoomSummary() { room.rx().liveRoomSummary() .execute { async -> diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt index 12dcb895..8fc268c3 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineEventController.kt @@ -16,12 +16,16 @@ package im.vector.riotredesign.features.home.room.detail.timeline +import android.view.View import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyAsyncUtil 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.room.timeline.TimelineEvent 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.features.home.LoadingItemModel_ 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 { it.id(event.localId) + it.setOnVisibilityStateChanged(OnModelVisibilityStateChangedListener { model, view, visibilityState -> + if (visibilityState == VisibilityState.VISIBLE) { + callback?.onEventVisible(event, currentPosition) + } + }) epoxyModels.add(it) } if (addDaySeparator) { @@ -98,6 +107,7 @@ class TimelineEventController(private val roomId: String, interface Callback { + fun onEventVisible(event: TimelineEvent, index: Int) fun onUrlClicked(url: String) } diff --git a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt index 8c38e2ec..cc30b010 100644 --- a/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt +++ b/app/src/main/java/im/vector/riotredesign/features/home/room/detail/timeline/TimelineItemFactory.kt @@ -16,9 +16,9 @@ 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.room.timeline.TimelineEvent +import im.vector.riotredesign.core.epoxy.KotlinModel class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, private val roomNameItemFactory: RoomNameItemFactory, @@ -28,14 +28,14 @@ class TimelineItemFactory(private val messageItemFactory: MessageItemFactory, fun create(event: TimelineEvent, nextEvent: TimelineEvent?, - callback: TimelineEventController.Callback?): EpoxyModel<*>? { + callback: TimelineEventController.Callback?): KotlinModel? { return when (event.root.type) { EventType.MESSAGE -> messageItemFactory.create(event, nextEvent, callback) EventType.STATE_ROOM_NAME -> roomNameItemFactory.create(event) EventType.STATE_ROOM_TOPIC -> roomTopicItemFactory.create(event) EventType.STATE_ROOM_MEMBER -> roomMemberItemFactory.create(event) - else -> defaultItemFactory.create(event) + else -> defaultItemFactory.create(event) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt index 102967e7..b7b78bc8 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -18,14 +18,24 @@ package im.vector.matrix.android.api.session.room.read 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 { - fun markLatestAsRead(callback: MatrixCallback) - + /** + * Force the read marker to be set on the latest event. + */ fun markAllAsRead(callback: MatrixCallback) + /** + * Set the read receipt on the event with provided eventId. + */ fun setReadReceipt(eventId: String, callback: MatrixCallback) - fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback) + /** + * Set the read marker on the event with provided eventId. + */ + fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt index 97037a92..d9abf791 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/di/NetworkModule.kt @@ -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.NetworkConnectivityChecker +import im.vector.matrix.android.internal.network.UnitConverterFactory import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import okreplay.OkReplayInterceptor import org.koin.dsl.module.module -import retrofit2.Converter import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import timber.log.Timber @@ -62,10 +62,6 @@ class NetworkModule { MoshiProvider.providesMoshi() } - single { - MoshiConverterFactory.create(get()) as Converter.Factory - } - single { NetworkConnectivityChecker(get()) } @@ -73,7 +69,8 @@ class NetworkModule { factory { Retrofit.Builder() .client(get()) - .addConverterFactory(get()) + .addConverterFactory(UnitConverterFactory) + .addConverterFactory(MoshiConverterFactory.create(get())) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt new file mode 100644 index 00000000..74d9623a --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/network/UnitConverterFactory.kt @@ -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, + retrofit: Retrofit): Converter? { + return if (type == Unit::class.java) UnitConverter else null + } + + private object UnitConverter : Converter { + override fun convert(value: ResponseBody) { + value.close() + } + } +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt index 5655886e..91da7dd5 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/SessionModule.kt @@ -47,6 +47,10 @@ internal class SessionModule(private val sessionParams: SessionParams) { sessionParams } + scope(DefaultSession.SCOPE) { + sessionParams.credentials + } + scope(DefaultSession.SCOPE) { val context = get() val childPath = sessionParams.credentials.userId.md5() diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt index 0b0bae8d..b1462789 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/DefaultRoomService.kt @@ -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.RoomSummaryEntityFields 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, private val roomFactory: RoomFactory) : RoomService { 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) } 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 bba9c134..ecb4d42d 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 @@ -112,7 +112,7 @@ internal interface RoomAPI { * @param markers the read markers */ @POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/read_markers") - fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map): Call + fun sendReadMarker(@Path("roomId") roomId: String, @Body markers: Map): Call } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt index 82c2f024..6b2c796c 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomFactory.kt @@ -17,6 +17,7 @@ package im.vector.matrix.android.internal.session.room 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.internal.session.room.members.LoadRoomMembersTask 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, private val monarchy: Monarchy, + private val credentials: Credentials, private val paginationTask: PaginationTask, private val contextOfEventTask: GetContextOfEventTask, private val setReadMarkersTask: SetReadMarkersTask, diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt index df870b23..716fd155 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/RoomModule.kt @@ -16,7 +16,6 @@ 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.room.members.DefaultLoadRoomMembersTask import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTask @@ -58,16 +57,15 @@ class RoomModule { } scope(DefaultSession.SCOPE) { - DefaultSetReadMarkersTask(get()) as SetReadMarkersTask + DefaultSetReadMarkersTask(get(), get(),get()) as SetReadMarkersTask } scope(DefaultSession.SCOPE) { - val sessionParams = get() - EventFactory(sessionParams.credentials) + EventFactory(get()) } scope(DefaultSession.SCOPE) { - RoomFactory(get(), get(), get(), get(), get(), get(), get()) + RoomFactory(get(), get(), get(), get(), get(), get(), get(), get()) } } diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt index e5d762eb..467e6117 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -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.task.TaskExecutor 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, private val monarchy: Monarchy, private val setReadMarkersTask: SetReadMarkersTask, private val taskExecutor: TaskExecutor) : ReadService { - override fun markLatestAsRead(callback: MatrixCallback) { - val lastEvent = getLatestEvent() - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = lastEvent?.eventId) - setReadMarkersTask.configureWith(params).executeBy(taskExecutor) - } - override fun markAllAsRead(callback: MatrixCallback) { - val lastEvent = getLatestEvent() - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = lastEvent?.eventId, readReceiptEventId = null) + val latestEvent = getLatestEvent() + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = latestEvent?.eventId, readReceiptEventId = latestEvent?.eventId) setReadMarkersTask.configureWith(params).executeBy(taskExecutor) } @@ -47,13 +41,14 @@ internal class DefaultReadService(private val roomId: String, setReadMarkersTask.configureWith(params).executeBy(taskExecutor) } - override fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback) { - val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = readReceiptEventId) + override fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = null) setReadMarkersTask.configureWith(params).executeBy(taskExecutor) } private fun getLatestEvent(): EventEntity? { - return monarchy.fetchManaged { EventEntity.latestEvent(it, roomId) } + return monarchy.fetchCopied { EventEntity.latestEvent(it, roomId) } } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt index 2f639377..92e44155 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -17,11 +17,22 @@ package im.vector.matrix.android.internal.session.room.read 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.session.room.RoomAPI import im.vector.matrix.android.internal.task.Task +import im.vector.matrix.android.internal.util.tryTransactionAsync -internal interface SetReadMarkersTask : Task { +internal interface SetReadMarkersTask : Task { data class Params( val roomId: String, @@ -33,19 +44,54 @@ internal interface SetReadMarkersTask : Task { private const val READ_MARKER = "m.fully_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 { - override fun execute(params: SetReadMarkersTask.Params): Try { + override fun execute(params: SetReadMarkersTask.Params): Try { val markers = HashMap() if (params.fullyReadEventId?.isNotEmpty() == true) { 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 } - return executeRequest { - apiCall = roomAPI.sendReadMarker(params.roomId, markers) + return if (markers.isEmpty()) { + 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 + } + } \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt index 8750385e..809ba16b 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/util/Monarchy.kt @@ -34,10 +34,10 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit): } } -fun Monarchy.fetchManaged(query: (Realm) -> T?): T? { +fun Monarchy.fetchCopied(query: (Realm) -> T?): T? { val ref = AtomicReference() doWithRealm { realm -> - val result = query.invoke(realm) + val result = query.invoke(realm)?.let { realm.copyFromRealm(it) } ref.set(result) } return ref.get()