Skip to content

Commit

Permalink
Possibility to mark rooms as unread
Browse files Browse the repository at this point in the history
Using com.famedly.marked_unnread, as per MSC2867
(matrix-org/matrix-spec-proposals#2867)

TODO:
- Currently, when upgrading from an older version, already existing
  unread flags are ignored until cache is cleared manually

Change-Id: I3b66fadb134c96f0eb428afd673035d790c16340
  • Loading branch information
SpiritCroc committed Dec 9, 2020
1 parent 3defe44 commit 3e4f6a4
Show file tree
Hide file tree
Showing 33 changed files with 325 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ object EventType {
const val PLUMBING = "m.room.plumbing"
const val BOT_OPTIONS = "m.room.bot.options"
const val PREVIEW_URLS = "org.matrix.room.preview_urls"
const val MARKED_UNREAD = "com.famedly.marked_unread"

// State Events

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ data class RoomSummary constructor(
val hasUnreadMessages: Boolean = false,
val hasUnreadContentMessages: Boolean = false,
val hasUnreadOriginalContentMessages: Boolean = false,
val markedUnread: Boolean = false,
val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE,
Expand Down Expand Up @@ -78,6 +79,10 @@ data class RoomSummary constructor(
val canStartCall: Boolean
get() = joinedMembersCount == 2

fun scIsUnread(preferenceProvider: RoomSummaryPreferenceProvider?): Boolean {
return markedUnread || scHasUnreadMessages(preferenceProvider)
}

fun scHasUnreadMessages(preferenceProvider: RoomSummaryPreferenceProvider?): Boolean {
if (preferenceProvider == null) {
// Fallback to default
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ interface ReadService {
*/
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>)

/**
* Mark a room as unread, or remove an existing unread marker.
*/
fun setMarkedUnread(markedUnread: Boolean, callback: MatrixCallback<Unit>)

/**
* Check if an event is already read, ie. your read receipt is set on a more recent event.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,40 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {

companion object {
const val SESSION_STORE_SCHEMA_VERSION = 5L
// SC-specific DB changes on top of Element
// 1: added markedUnread field
const val SESSION_STORE_SCHEMA_SC_VERSION = 1L
const val SESSION_STORE_SCHEMA_SC_VERSION_OFFSET = (1L shl 12)

const val SESSION_STORE_SCHEMA_VERSION = 5L +
SESSION_STORE_SCHEMA_SC_VERSION * SESSION_STORE_SCHEMA_SC_VERSION_OFFSET

}

override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
override fun migrate(realm: DynamicRealm, combinedOldVersion: Long, newVersion: Long) {
val oldVersion = combinedOldVersion % SESSION_STORE_SCHEMA_SC_VERSION_OFFSET
val oldScVersion = combinedOldVersion / SESSION_STORE_SCHEMA_SC_VERSION_OFFSET

Timber.v("Migrating Realm Session from $oldVersion to $newVersion")

if (oldVersion <= 0) migrateTo1(realm)
if (oldVersion <= 1) migrateTo2(realm)
if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(realm)

if (oldScVersion <= 0) migrateToSc1(realm)
}

// SC Version 1L added markedUnread
private fun migrateToSc1(realm: DynamicRealm) {
Timber.d("Step SC 0 -> 1")
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.MARKED_UNREAD, Boolean::class.java)
}



private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1")
// Add hasFailedSending in RoomSummary and a small warning icon on room list
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
hasUnreadContentMessages = roomSummaryEntity.hasUnreadContentMessages,
hasUnreadOriginalContentMessages = roomSummaryEntity.hasUnreadOriginalContentMessages,
markedUnread = roomSummaryEntity.markedUnread,
tags = tags,
typingUsers = typingUsers,
membership = roomSummaryEntity.membership,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ internal open class RoomSummaryEntity(
var hasUnreadMessages: Boolean = false,
var hasUnreadContentMessages: Boolean = false,
var hasUnreadOriginalContentMessages: Boolean = false,
var markedUnread: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import io.realm.Realm
import io.realm.RealmConfiguration
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity

internal fun isEventRead(realmConfiguration: RealmConfiguration,
userId: String?,
Expand Down Expand Up @@ -75,3 +76,13 @@ internal fun isReadMarkerMoreRecent(realmConfiguration: RealmConfiguration,
}
}
}
internal fun isMarkedUnread(realmConfiguration: RealmConfiguration,
roomId: String?): Boolean {
if (roomId.isNullOrBlank()) {
return false
}
return Realm.getInstance(realmConfiguration).use { realm ->
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
roomSummary?.markedUnread ?: false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoom
import org.matrix.android.sdk.internal.session.room.membership.threepid.DefaultInviteThreePidTask
import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask
import org.matrix.android.sdk.internal.session.room.read.DefaultMarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.read.DefaultSetMarkedUnreadTask
import org.matrix.android.sdk.internal.session.room.read.DefaultSetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.room.read.SetMarkedUnreadTask
import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.relation.DefaultFetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.DefaultFindReactionEventForUndoTask
Expand Down Expand Up @@ -154,6 +156,9 @@ internal abstract class RoomModule {
@Binds
abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask

@Binds
abstract fun bindSetMarkedUnreadTask(task: DefaultSetMarkedUnreadTask): SetMarkedUnreadTask

@Binds
abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ internal class DefaultReadService @AssistedInject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask,
private val setMarkedUnreadTask: SetMarkedUnreadTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String
) : ReadService {
Expand All @@ -62,6 +63,8 @@ internal class DefaultReadService @AssistedInject constructor(
this.callback = callback
}
.executeBy(taskExecutor)
// Automatically unset unread marker
setMarkedUnreadFlag(false, callback)
}

override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) {
Expand All @@ -82,6 +85,25 @@ internal class DefaultReadService @AssistedInject constructor(
.executeBy(taskExecutor)
}

override fun setMarkedUnread(markedUnread: Boolean, callback: MatrixCallback<Unit>) {
if (markedUnread) {
setMarkedUnreadFlag(true, callback)
} else {
// We want to both remove unread marker and update read receipt position,
// i.e., we want what markAsRead does
markAsRead(ReadService.MarkAsReadParams.READ_RECEIPT, callback)
}
}

private fun setMarkedUnreadFlag(markedUnread: Boolean, callback: MatrixCallback<Unit>) {
val params = SetMarkedUnreadTask.Params(roomId, markedUnread = markedUnread)
setMarkedUnreadTask
.configureWith(params) {
this.callback = callback
}
.executeBy(taskExecutor)
}

override fun isEventRead(eventId: String): Boolean {
return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,14 @@ internal interface MarkAllRoomsReadTask : Task<MarkAllRoomsReadTask.Params, Unit
)
}

internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask) : MarkAllRoomsReadTask {
internal class DefaultMarkAllRoomsReadTask @Inject constructor(private val readMarkersTask: SetReadMarkersTask, private val markUnreadTask: SetMarkedUnreadTask) : MarkAllRoomsReadTask {

override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
params.roomIds.forEach { roomId ->
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true))
}
params.roomIds.forEach { roomId ->
markUnreadTask.execute(SetMarkedUnreadTask.Params(roomId, markedUnread = false))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.read

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class MarkedUnreadContent(
@Json(name = "unread") val markedUnread: Boolean
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.read

import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.di.UserId
import org.matrix.android.sdk.internal.network.executeRequest
import org.matrix.android.sdk.internal.task.Task
import org.matrix.android.sdk.internal.util.awaitTransaction
import org.greenrobot.eventbus.EventBus
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.internal.database.query.isMarkedUnread
import org.matrix.android.sdk.internal.session.sync.RoomMarkedUnreadHandler
import org.matrix.android.sdk.internal.session.user.accountdata.AccountDataAPI
import timber.log.Timber
import javax.inject.Inject

internal interface SetMarkedUnreadTask : Task<SetMarkedUnreadTask.Params, Unit> {

data class Params(
val roomId: String,
val markedUnread: Boolean,
val markedUnreadContent: MarkedUnreadContent = MarkedUnreadContent(markedUnread)
)
}

internal class DefaultSetMarkedUnreadTask @Inject constructor(
private val accountDataApi: AccountDataAPI,
@SessionDatabase private val monarchy: Monarchy,
private val roomMarkedUnreadHandler: RoomMarkedUnreadHandler,
@UserId private val userId: String,
private val eventBus: EventBus
) : SetMarkedUnreadTask {

override suspend fun execute(params: SetMarkedUnreadTask.Params) {
Timber.v("Execute set marked unread with params: $params")

if (isMarkedUnread(monarchy.realmConfiguration, params.roomId) != params.markedUnread) {
updateDatabase(params.roomId, params.markedUnread)
executeRequest<Unit>(eventBus) {
isRetryable = true
apiCall = accountDataApi.setRoomAccountData(userId, params.roomId, EventType.MARKED_UNREAD, params.markedUnreadContent)
}
}
}

private suspend fun updateDatabase(roomId: String, markedUnread: Boolean) {
monarchy.awaitTransaction { realm ->
roomMarkedUnreadHandler.handle(realm, roomId, MarkedUnreadContent(markedUnread))
val roomSummary = RoomSummaryEntity.where(realm, roomId).findFirst()
?: return@awaitTransaction
roomSummary.markedUnread = markedUnread
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.sync

import org.matrix.android.sdk.internal.session.room.read.MarkedUnreadContent
import io.realm.Realm
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.getOrCreate
import timber.log.Timber
import javax.inject.Inject

internal class RoomMarkedUnreadHandler @Inject constructor() {

fun handle(realm: Realm, roomId: String, content: MarkedUnreadContent?) {
if (content == null) {
return
}
Timber.v("Handle for roomId: $roomId markedUnread: ${content.markedUnread}")

RoomSummaryEntity.getOrCreate(realm, roomId).apply {
markedUnread = content.markedUnread
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import org.matrix.android.sdk.internal.session.mapWithProgress
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler
import org.matrix.android.sdk.internal.session.room.read.FullyReadContent
import org.matrix.android.sdk.internal.session.room.read.MarkedUnreadContent
import org.matrix.android.sdk.internal.session.room.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection
Expand All @@ -70,6 +71,7 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
private val roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler,
private val roomMarkedUnreadHandler: RoomMarkedUnreadHandler,
private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler,
Expand Down Expand Up @@ -407,6 +409,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content)
} else if (eventType == EventType.MARKED_UNREAD) {
val content = event.getClearContent().toModel<MarkedUnreadContent>()
roomMarkedUnreadHandler.handle(realm, roomId, content)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,18 @@ interface AccountDataAPI {
fun setAccountData(@Path("userId") userId: String,
@Path("type") type: String,
@Body params: Any): Call<Unit>

/**
* Set some room account_data for the client.
*
* @param userId the user id
* @param roomId the room id
* @param type the type
* @param params the put params
*/
@PUT(NetworkConstants.URI_API_PREFIX_PATH_R0 + "user/{userId}/rooms/{roomId}/account_data/{type}")
fun setRoomAccountData(@Path("userId") userId: String,
@Path("roomId") roomId: String,
@Path("type") type: String,
@Body params: Any): Call<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,9 @@ class BreadcrumbsController @Inject constructor(
avatarRenderer(avatarRenderer)
matrixItem(it.toMatrixItem())
unreadNotificationCount(it.notificationCount)
markedUnread(it.markedUnread)
showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.scHasUnreadMessages(scSdkPreferences))
hasUnreadMessage(it.scIsUnread(scSdkPreferences))
hasDraft(it.userDrafts.isNotEmpty())
itemClickListener(
DebouncedClickListener(View.OnClickListener { _ ->
Expand Down
Loading

0 comments on commit 3e4f6a4

Please sign in to comment.