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 537a23f817..57ca350888 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 @@ -50,6 +50,7 @@ class RoomDetailViewModel(initialState: RoomDetailViewState, observeRoomSummary() observeTimeline() room.loadRoomMembersIfNeeded() + room.markLatestAsRead(callback = object : MatrixCallback {}) } fun accept(action: RoomDetailActions) { diff --git a/app/src/main/res/drawable/bg_unread_notification.xml b/app/src/main/res/drawable/bg_unread_notification.xml index d6320a3722..496134a791 100644 --- a/app/src/main/res/drawable/bg_unread_notification.xml +++ b/app/src/main/res/drawable/bg_unread_notification.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_room.xml b/app/src/main/res/layout/item_room.xml index 9179bd2b4f..c0148ee715 100644 --- a/app/src/main/res/layout/item_room.xml +++ b/app/src/main/res/layout/item_room.xml @@ -52,7 +52,7 @@ android:gravity="center" android:minWidth="24dp" android:textColor="@android:color/white" - android:textSize="10sp" + android:textSize="12sp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9f20f5f9f3..9797d9459d 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -16,4 +16,5 @@ #a5aab2 #ebedf8 #a5a5a5 + #61708B diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt index bcb1e9dc62..fb27b29a28 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/Room.kt @@ -19,6 +19,7 @@ package im.vector.matrix.android.api.session.room import androidx.lifecycle.LiveData import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.util.Cancelable @@ -26,7 +27,7 @@ import im.vector.matrix.android.api.util.Cancelable /** * This interface defines methods to interact within a room. */ -interface Room : TimelineService, SendService { +interface Room : TimelineService, SendService, ReadService { /** * The roomId of this room 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 new file mode 100644 index 0000000000..102967e777 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/api/session/room/read/ReadService.kt @@ -0,0 +1,31 @@ +/* + * 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.api.session.room.read + +import im.vector.matrix.android.api.MatrixCallback + +interface ReadService { + + fun markLatestAsRead(callback: MatrixCallback) + + fun markAllAsRead(callback: MatrixCallback) + + fun setReadReceipt(eventId: String, callback: MatrixCallback) + + fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback) + +} \ No newline at end of file diff --git a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt index 48bcd0b34a..d6efe17851 100644 --- a/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/database/query/EventEntityQueries.kt @@ -16,6 +16,7 @@ package im.vector.matrix.android.internal.database.query +import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity.LinkFilterMode.* import im.vector.matrix.android.internal.database.model.EventEntityFields @@ -48,6 +49,12 @@ internal fun EventEntity.Companion.where(realm: Realm, } } +internal fun EventEntity.Companion.latestEvent(realm: Realm, + roomId: String): EventEntity? { + val chunkEntity = ChunkEntity.findLastLiveChunkFromRoom(realm, roomId) + return chunkEntity?.events?.where()?.sort(EventEntityFields.DISPLAY_INDEX)?.findFirst() +} + internal fun RealmQuery.next(from: Int? = null, strict: Boolean = true): EventEntity? { if (from != null) { @@ -62,7 +69,6 @@ internal fun RealmQuery.next(from: Int? = null, strict: Boolean = t .findFirst() } - internal fun RealmQuery.last(since: Int? = null, strict: Boolean = false): EventEntity? { if (since != null) { if (strict) { 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 9a0bad451d..5b716d4ea6 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 @@ -22,10 +22,11 @@ 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.session.room.Room -import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.RoomSummary +import im.vector.matrix.android.api.session.room.read.ReadService +import im.vector.matrix.android.api.session.room.send.SendService import im.vector.matrix.android.api.session.room.timeline.TimelineData import im.vector.matrix.android.api.session.room.timeline.TimelineService import im.vector.matrix.android.api.util.Cancelable @@ -49,6 +50,7 @@ internal data class DefaultRoom( private val monarchy by inject() private val timelineService by inject { parametersOf(roomId) } private val sendService by inject { parametersOf(roomId) } + private val readService by inject { parametersOf(roomId) } private val taskExecutor by inject() override val roomSummary: LiveData by lazy { @@ -76,5 +78,21 @@ internal data class DefaultRoom( return sendService.sendTextMessage(text, callback) } + override fun setReadReceipt(eventId: String, callback: MatrixCallback) { + readService.setReadReceipt(eventId, callback) + } + + override fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback) { + readService.setReadMarkers(fullyReadEventId, readReceiptEventId, callback) + } + + override fun markAllAsRead(callback: MatrixCallback) { + readService.markAllAsRead(callback) + } + + override fun markLatestAsRead(callback: MatrixCallback) { + readService.markLatestAsRead(callback) + } + } \ No newline at end of file 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 b14b0fcf0b..bba9c13490 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 @@ -24,7 +24,12 @@ 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.PaginationResponse import retrofit2.Call -import retrofit2.http.* +import retrofit2.http.Body +import retrofit2.http.GET +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Path +import retrofit2.http.Query internal interface RoomAPI { @@ -100,5 +105,14 @@ internal interface RoomAPI { @GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "rooms/{roomId}/event/{eventId}") fun getEvent(@Path("roomId") roomId: String, @Path("eventId") eventId: String): Call + /** + * Send read markers. + * + * @param roomId the room id + * @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 + } \ No newline at end of file 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 3ebaa0ee6e..65eb76b932 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 @@ -17,14 +17,18 @@ package im.vector.matrix.android.internal.session.room import im.vector.matrix.android.api.auth.data.SessionParams +import im.vector.matrix.android.api.session.room.read.ReadService import im.vector.matrix.android.api.session.room.send.SendService -import im.vector.matrix.android.internal.session.room.send.EventFactory import im.vector.matrix.android.api.session.room.timeline.TimelineService 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 import im.vector.matrix.android.internal.session.room.members.RoomMemberExtractor +import im.vector.matrix.android.internal.session.room.read.DefaultReadService +import im.vector.matrix.android.internal.session.room.read.DefaultSetReadMarkersTask +import im.vector.matrix.android.internal.session.room.read.SetReadMarkersTask import im.vector.matrix.android.internal.session.room.send.DefaultSendService +import im.vector.matrix.android.internal.session.room.send.EventFactory import im.vector.matrix.android.internal.session.room.timeline.DefaultGetContextOfEventTask import im.vector.matrix.android.internal.session.room.timeline.DefaultPaginationTask import im.vector.matrix.android.internal.session.room.timeline.DefaultTimelineService @@ -63,6 +67,10 @@ class RoomModule { DefaultGetContextOfEventTask(get(), get()) as GetContextOfEventTask } + scope(DefaultSession.SCOPE) { + DefaultSetReadMarkersTask(get()) as SetReadMarkersTask + } + scope(DefaultSession.SCOPE) { val sessionParams = get() EventFactory(sessionParams.credentials) @@ -79,5 +87,9 @@ class RoomModule { DefaultSendService(roomId, get(), get()) as SendService } + factory { (roomId: String) -> + DefaultReadService(roomId, get(), get(), get()) as ReadService + } + } } 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 new file mode 100644 index 0000000000..e5d762eb75 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/DefaultReadService.kt @@ -0,0 +1,59 @@ +/* + * 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.session.room.read + +import com.zhuinden.monarchy.Monarchy +import im.vector.matrix.android.api.MatrixCallback +import im.vector.matrix.android.api.session.room.read.ReadService +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 + +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) + setReadMarkersTask.configureWith(params).executeBy(taskExecutor) + } + + override fun setReadReceipt(eventId: String, callback: MatrixCallback) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = null, readReceiptEventId = eventId) + setReadMarkersTask.configureWith(params).executeBy(taskExecutor) + } + + override fun setReadMarkers(fullyReadEventId: String, readReceiptEventId: String?, callback: MatrixCallback) { + val params = SetReadMarkersTask.Params(roomId, fullyReadEventId = fullyReadEventId, readReceiptEventId = readReceiptEventId) + setReadMarkersTask.configureWith(params).executeBy(taskExecutor) + } + + private fun getLatestEvent(): EventEntity? { + return monarchy.fetchManaged { 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 new file mode 100644 index 0000000000..2f639377f3 --- /dev/null +++ b/matrix-sdk-android/src/main/java/im/vector/matrix/android/internal/session/room/read/SetReadMarkersTask.kt @@ -0,0 +1,51 @@ +/* + * 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.session.room.read + +import arrow.core.Try +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 + +internal interface SetReadMarkersTask : Task { + + data class Params( + val roomId: String, + val fullyReadEventId: String?, + val readReceiptEventId: String? + ) +} + +private const val READ_MARKER = "m.fully_read" +private const val READ_RECEIPT = "m.read" + +internal class DefaultSetReadMarkersTask(private val roomAPI: RoomAPI +) : SetReadMarkersTask { + + 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) { + markers[READ_RECEIPT] = params.readReceiptEventId + } + return executeRequest { + apiCall = roomAPI.sendReadMarker(params.roomId, markers) + } + } +} \ 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 2481332080..8750385eed 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 @@ -19,6 +19,8 @@ package im.vector.matrix.android.internal.util import arrow.core.Try import com.zhuinden.monarchy.Monarchy import io.realm.Realm +import io.realm.RealmModel +import java.util.concurrent.atomic.AtomicReference internal fun Monarchy.tryTransactionSync(transaction: (realm: Realm) -> Unit): Try { return Try { @@ -30,4 +32,13 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit): return Try { this.writeAsync(transaction) } +} + +fun Monarchy.fetchManaged(query: (Realm) -> T?): T? { + val ref = AtomicReference() + doWithRealm { realm -> + val result = query.invoke(realm) + ref.set(result) + } + return ref.get() } \ No newline at end of file