diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt index 0a7f3ff09f1..2b930dbd036 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/EventType.kt @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt index 910f352746c..aabcc379fe7 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt @@ -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 = emptyList(), val membership: Membership = Membership.NONE, val versioningState: VersioningState = VersioningState.NONE, @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt index 4f44c9a9126..b32280d6398 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/read/ReadService.kt @@ -47,6 +47,11 @@ interface ReadService { */ fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback) + /** + * Mark a room as unread, or remove an existing unread marker. + */ + fun setMarkedUnread(markedUnread: Boolean, callback: MatrixCallback) + /** * Check if an event is already read, ie. your read receipt is set on a more recent event. */ diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 973388da49d..34689adaf49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -27,10 +27,20 @@ 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) @@ -38,8 +48,19 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { 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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 2a05c712011..a7a0a8e6c59 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -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, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt index c6bf3d12d5f..d669e05935c 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt @@ -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 = RealmList(), var userDrafts: UserDraftsEntity? = null, var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt index a3c741ad55a..20c17c6a082 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/ReadQueries.kt @@ -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?, @@ -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 + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt index 6381796ee04..cbad0407ef2 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/RoomModule.kt @@ -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 @@ -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 diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt index 025bea09f4c..d7796d90f1f 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/DefaultReadService.kt @@ -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 { @@ -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) { @@ -82,6 +85,25 @@ internal class DefaultReadService @AssistedInject constructor( .executeBy(taskExecutor) } + override fun setMarkedUnread(markedUnread: Boolean, callback: MatrixCallback) { + 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) { + 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) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt index ebf034a7752..fc7bc3e5d56 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkAllRoomsReadTask.kt @@ -25,11 +25,14 @@ internal interface MarkAllRoomsReadTask : Task readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)) } + params.roomIds.forEach { roomId -> + markUnreadTask.execute(SetMarkedUnreadTask.Params(roomId, markedUnread = false)) + } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkedUnreadContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkedUnreadContent.kt new file mode 100644 index 00000000000..cf63a8abc84 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/MarkedUnreadContent.kt @@ -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 +) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt new file mode 100644 index 00000000000..e98d5d93812 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/read/SetMarkedUnreadTask.kt @@ -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 { + + 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(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 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomMarkedUnreadHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomMarkedUnreadHandler.kt new file mode 100644 index 00000000000..bf4d991b0fc --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomMarkedUnreadHandler.kt @@ -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 + } + } +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt index b1b2f65dc2f..2733edfecfd 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/RoomSyncHandler.kt @@ -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 @@ -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, @@ -407,6 +409,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle } else if (eventType == EventType.FULLY_READ) { val content = event.getClearContent().toModel() roomFullyReadHandler.handle(realm, roomId, content) + } else if (eventType == EventType.MARKED_UNREAD) { + val content = event.getClearContent().toModel() + roomMarkedUnreadHandler.handle(realm, roomId, content) } } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt index 3de484fab34..4c3954c956d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/user/accountdata/AccountDataAPI.kt @@ -35,4 +35,18 @@ interface AccountDataAPI { fun setAccountData(@Path("userId") userId: String, @Path("type") type: String, @Body params: Any): Call + + /** + * 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 } diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt index cf56f6d531f..283b2a1fe3e 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsController.kt @@ -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 { _ -> diff --git a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt index 15d4d827e64..5359d3a5461 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/breadcrumbs/BreadcrumbsItem.kt @@ -37,6 +37,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel() { @EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadMessages: Int = 0 // SC addition + @EpoxyAttribute var markedUnread: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false @@ -47,7 +48,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel() { holder.rootView.setOnClickListener(itemClickListener) holder.unreadIndentIndicator.isVisible = hasUnreadMessage avatarRenderer.render(matrixItem, holder.avatarImageView) - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, unreadMessages)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, unreadMessages, markedUnread)) holder.draftIndentIndicator.isVisible = hasDraft holder.typingIndicator.isVisible = hasTypingUsers } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 920ab5f2ea1..9e739fa6366 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -1217,7 +1217,7 @@ class RoomDetailFragment @Inject constructor( val inviter = state.asyncInviter() if (summary?.membership == Membership.JOIN) { jumpToBottomView.count = summary.notificationCount - jumpToBottomView.drawBadge = summary.scHasUnreadMessages(ScSdkPreferences(context)) + jumpToBottomView.drawBadge = summary.scIsUnread(ScSdkPreferences(context)) scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline timelineEventController.update(state) inviteView.visibility = View.GONE diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt index 772b7992e60..8c241d17610 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomCategoryItem.kt @@ -35,6 +35,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadMessages: Int = 0 @EpoxyAttribute var showHighlighted: Boolean = false + @EpoxyAttribute var markedUnread: Boolean = false @EpoxyAttribute var listener: (() -> Unit)? = null override fun bind(holder: Holder) { @@ -44,7 +45,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel() { val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also { DrawableCompat.setTint(it, tintColor) } - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, unreadMessages)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, unreadMessages, markedUnread)) holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null) holder.titleView.text = title holder.rootView.setOnClickListener { listener?.invoke() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt index 4a6c1c16fcd..5592a70b2b2 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListAction.kt @@ -29,5 +29,6 @@ sealed class RoomListAction : VectorViewModelAction { data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() + data class SetMarkedUnread(val roomId: String, val markedUnread: Boolean) : RoomListAction() object MarkAllRoomsRead : RoomListAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt index b0df5009c7b..d5740e9f0f8 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListFragment.kt @@ -262,6 +262,12 @@ class RoomListFragment @Inject constructor( is RoomListQuickActionsSharedAction.LowPriority -> { roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY)) } + is RoomListQuickActionsSharedAction.MarkUnread -> { + roomListViewModel.handle(RoomListAction.SetMarkedUnread(quickAction.roomId, true)) + } + is RoomListQuickActionsSharedAction.MarkRead -> { + roomListViewModel.handle(RoomListAction.SetMarkedUnread(quickAction.roomId, false)) + } is RoomListQuickActionsSharedAction.Leave -> { AlertDialog.Builder(requireContext()) .setTitle(R.string.room_participants_leave_prompt_title) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt index 510d0a2b883..46658cfdd31 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewModel.kt @@ -84,6 +84,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ToggleTag -> handleToggleTag(action) + is RoomListAction.SetMarkedUnread -> handleSetMarkedUnread(action) }.exhaustive } @@ -222,6 +223,18 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, } } + private fun handleSetMarkedUnread(action: RoomListAction.SetMarkedUnread) { + session.getRoom(action.roomId)?.setMarkedUnread(action.markedUnread, object : MatrixCallback { + override fun onSuccess(data: Unit) { + _viewEvents.post(RoomListViewEvents.Done) + } + + override fun onFailure(failure: Throwable) { + _viewEvents.post(RoomListViewEvents.Failure(failure)) + } + }) + } + private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) { _viewEvents.post(RoomListViewEvents.Loading(null)) session.getRoom(action.roomId)?.leave(null, object : MatrixCallback { diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt index c1d4456c7fc..e6c6f3c8ea4 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListViewState.kt @@ -130,7 +130,7 @@ data class RoomListViewState( get() = asyncFilteredRooms.invoke() ?.flatMap { it.value } ?.filter { it.membership == Membership.JOIN } - ?.any { it.scHasUnreadMessages(scSdkPreferences) } + ?.any { it.scIsUnread(scSdkPreferences) } ?: false } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt index ec5e764977d..18a5f105420 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryController.kt @@ -134,14 +134,20 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri val unreadCount = if (summaries.isEmpty()) { 0 } else { - summaries.map { it.notificationCount }.sumBy { i -> i } + // Count notifications + number of chats with no notifications marked as unread + summaries.map { it }.sumBy { x -> if (x.notificationCount > 0) x.notificationCount else if (x.markedUnread) 1 else 0 } + } + val markedUnread = if (summaries.isEmpty()) { + false + } else { + summaries.map { it.markedUnread }.sumBy { b -> if (b) 1 else 0 } > 0 } // SC addition val unreadMessages = if (summaries.isEmpty() || !userPreferencesProvider.shouldShowUnimportantCounterBadge()) { 0 } else { // TODO actual sum of events instead of sum of chats? - summaries.map { it.scHasUnreadMessages(scSdkPreferences) }.sumBy { b -> if (b) 1 else 0 } + summaries.map { it.scIsUnread(scSdkPreferences) }.sumBy { b -> if (b) 1 else 0 } } val showHighlighted = summaries.any { it.highlightCount > 0 } roomCategoryItem { @@ -151,6 +157,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri unreadNotificationCount(unreadCount) unreadMessages(unreadMessages) showHighlighted(showHighlighted) + markedUnread(markedUnread) listener { mutateExpandedState() update(viewState) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt index 461374a85a3..08b8c9ae923 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItem.kt @@ -53,6 +53,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null @EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var hasUnreadMessage: Boolean = false + @EpoxyAttribute var markedUnread: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var hasFailedSending: Boolean = false @@ -71,7 +72,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent // SC-TODO: once we count unimportant unread messages, pass that as counter - for now, unreadIndentIndicator is enough, so pass 0 to the badge instead - holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, 0)) + holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted, 0, markedUnread)) holder.unreadIndentIndicator.isVisible = hasUnreadMessage holder.draftView.isVisible = hasDraft avatarRenderer.render(matrixItem, holder.avatarImageView) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt index 98da91effa8..5efd6c4d239 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomSummaryItemFactory.kt @@ -105,7 +105,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .showSelected(showSelected) .hasFailedSending(roomSummary.hasFailedSending) .unreadNotificationCount(unreadCount) - .hasUnreadMessage(roomSummary.scHasUnreadMessages(scSdkPreferences)) + .hasUnreadMessage(roomSummary.scIsUnread(scSdkPreferences)) + .markedUnread(roomSummary.markedUnread) .hasDraft(roomSummary.userDrafts.isNotEmpty()) .itemLongClickListener { _ -> onLongClick?.invoke(roomSummary) ?: false diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/UnreadCounterBadgeView.kt b/vector/src/main/java/im/vector/app/features/home/room/list/UnreadCounterBadgeView.kt index f318df0ddd5..680303b8a35 100755 --- a/vector/src/main/java/im/vector/app/features/home/room/list/UnreadCounterBadgeView.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/UnreadCounterBadgeView.kt @@ -30,11 +30,11 @@ class UnreadCounterBadgeView : AppCompatTextView { constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) fun render(state: State) { - if (state.count == 0 && state.unread == 0) { + if (state.count == 0 && state.unread == 0 && !state.markedUnread) { visibility = View.INVISIBLE } else { visibility = View.VISIBLE - val bgRes = if (state.count > 0) { + val bgRes = if (state.count > 0 || state.markedUnread) { if (state.highlighted) { R.drawable.bg_unread_highlight } else { @@ -44,7 +44,12 @@ class UnreadCounterBadgeView : AppCompatTextView { R.drawable.bg_unread_unimportant } setBackgroundResource(bgRes) - text = RoomSummaryFormatter.formatUnreadMessagesCounter(if (state.count > 0) state.count else state.unread) + text = if (state.count == 0 && state.markedUnread) + // Centered star (instead of "*") + //"\u2217" + "!" + else + RoomSummaryFormatter.formatUnreadMessagesCounter(if (state.count > 0) state.count else state.unread) } } @@ -52,6 +57,7 @@ class UnreadCounterBadgeView : AppCompatTextView { val count: Int, val highlighted: Boolean, // SC addition - val unread: Int + val unread: Int, + val markedUnread: Boolean ) } diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt index ebacdbd1eb8..a943435a46a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsEpoxyController.kt @@ -22,6 +22,8 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.home.room.ScSdkPreferences +import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.room.notification.RoomNotificationState import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject @@ -31,7 +33,8 @@ import javax.inject.Inject */ class RoomListQuickActionsEpoxyController @Inject constructor( private val avatarRenderer: AvatarRenderer, - private val stringProvider: StringProvider + private val stringProvider: StringProvider, + private val scSdkPreferences: ScSdkPreferences ) : TypedEpoxyController() { var listener: Listener? = null @@ -54,6 +57,16 @@ class RoomListQuickActionsEpoxyController @Inject constructor( lowPriorityClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.LowPriority(roomSummary.roomId)) } } + // Mark read/unread + dividerItem { + id("mark_unread_separator") + } + if (roomSummary.scIsUnread(scSdkPreferences)) { + RoomListQuickActionsSharedAction.MarkRead(roomSummary.roomId).toBottomSheetItem(-1) + } else { + RoomListQuickActionsSharedAction.MarkUnread(roomSummary.roomId).toBottomSheetItem(-1) + } + // Notifications dividerItem { id("notifications_separator") diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt index 075dca0c52f..13ec084a0f5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/actions/RoomListQuickActionsSharedAction.kt @@ -27,6 +27,16 @@ sealed class RoomListQuickActionsSharedAction( val destructive: Boolean = false) : VectorSharedAction { + data class MarkUnread(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_mark_room_unread, + R.drawable.ic_room_actions_mark_room_unread + ) + + data class MarkRead(val roomId: String) : RoomListQuickActionsSharedAction( + R.string.room_list_quick_actions_mark_room_read, + R.drawable.ic_room_actions_mark_room_read + ) + data class NotificationsAllNoisy(val roomId: String) : RoomListQuickActionsSharedAction( R.string.room_list_quick_actions_notifications_all_noisy, R.drawable.ic_room_actions_notifications_all_noisy diff --git a/vector/src/main/res/drawable/ic_room_actions_mark_room_read.xml b/vector/src/main/res/drawable/ic_room_actions_mark_room_read.xml new file mode 100644 index 00000000000..f95f7a646da --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_mark_room_read.xml @@ -0,0 +1,7 @@ + + + diff --git a/vector/src/main/res/drawable/ic_room_actions_mark_room_unread.xml b/vector/src/main/res/drawable/ic_room_actions_mark_room_unread.xml new file mode 100644 index 00000000000..c36799ceb9e --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_actions_mark_room_unread.xml @@ -0,0 +1,7 @@ + + + diff --git a/vector/src/main/res/values-de/strings_sc.xml b/vector/src/main/res/values-de/strings_sc.xml index 68cb9fa5769..8af06b6abe0 100644 --- a/vector/src/main/res/values-de/strings_sc.xml +++ b/vector/src/main/res/values-de/strings_sc.xml @@ -59,5 +59,7 @@ Rauminfo Teilnehmer + Als ungelesen markieren + Als gelesen markieren diff --git a/vector/src/main/res/values/strings_sc.xml b/vector/src/main/res/values/strings_sc.xml index 0238c879b10..4a6fcd6d30b 100644 --- a/vector/src/main/res/values/strings_sc.xml +++ b/vector/src/main/res/values/strings_sc.xml @@ -59,5 +59,7 @@ Room details Participants + Mark as unread + Mark as read