Possibility to mark rooms as unread

Using com.famedly.marked_unnread, as per MSC2867
(https://github.com/matrix-org/matrix-doc/pull/2867)

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

Change-Id: I3b66fadb134c96f0eb428afd673035d790c16340
This commit is contained in:
SpiritCroc 2020-11-23 13:15:33 +01:00
parent 3defe44eff
commit 3fb2df76c2
33 changed files with 325 additions and 17 deletions

View file

@ -35,6 +35,7 @@ object EventType {
const val PLUMBING = "m.room.plumbing" const val PLUMBING = "m.room.plumbing"
const val BOT_OPTIONS = "m.room.bot.options" const val BOT_OPTIONS = "m.room.bot.options"
const val PREVIEW_URLS = "org.matrix.room.preview_urls" const val PREVIEW_URLS = "org.matrix.room.preview_urls"
const val MARKED_UNREAD = "com.famedly.marked_unread"
// State Events // State Events

View file

@ -47,6 +47,7 @@ data class RoomSummary constructor(
val hasUnreadMessages: Boolean = false, val hasUnreadMessages: Boolean = false,
val hasUnreadContentMessages: Boolean = false, val hasUnreadContentMessages: Boolean = false,
val hasUnreadOriginalContentMessages: Boolean = false, val hasUnreadOriginalContentMessages: Boolean = false,
val markedUnread: Boolean = false,
val tags: List<RoomTag> = emptyList(), val tags: List<RoomTag> = emptyList(),
val membership: Membership = Membership.NONE, val membership: Membership = Membership.NONE,
val versioningState: VersioningState = VersioningState.NONE, val versioningState: VersioningState = VersioningState.NONE,
@ -78,6 +79,10 @@ data class RoomSummary constructor(
val canStartCall: Boolean val canStartCall: Boolean
get() = joinedMembersCount == 2 get() = joinedMembersCount == 2
fun scIsUnread(preferenceProvider: RoomSummaryPreferenceProvider?): Boolean {
return markedUnread || scHasUnreadMessages(preferenceProvider)
}
fun scHasUnreadMessages(preferenceProvider: RoomSummaryPreferenceProvider?): Boolean { fun scHasUnreadMessages(preferenceProvider: RoomSummaryPreferenceProvider?): Boolean {
if (preferenceProvider == null) { if (preferenceProvider == null) {
// Fallback to default // Fallback to default

View file

@ -47,6 +47,11 @@ interface ReadService {
*/ */
fun setReadMarker(fullyReadEventId: String, callback: MatrixCallback<Unit>) 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. * Check if an event is already read, ie. your read receipt is set on a more recent event.
*/ */

View file

@ -27,10 +27,20 @@ import javax.inject.Inject
class RealmSessionStoreMigration @Inject constructor() : RealmMigration { class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
companion object { 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") Timber.v("Migrating Realm Session from $oldVersion to $newVersion")
if (oldVersion <= 0) migrateTo1(realm) if (oldVersion <= 0) migrateTo1(realm)
@ -38,8 +48,19 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
if (oldVersion <= 2) migrateTo3(realm) if (oldVersion <= 2) migrateTo3(realm)
if (oldVersion <= 3) migrateTo4(realm) if (oldVersion <= 3) migrateTo4(realm)
if (oldVersion <= 4) migrateTo5(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) { private fun migrateTo1(realm: DynamicRealm) {
Timber.d("Step 0 -> 1") Timber.d("Step 0 -> 1")
// Add hasFailedSending in RoomSummary and a small warning icon on room list // Add hasFailedSending in RoomSummary and a small warning icon on room list

View file

@ -60,6 +60,7 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages, hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
hasUnreadContentMessages = roomSummaryEntity.hasUnreadContentMessages, hasUnreadContentMessages = roomSummaryEntity.hasUnreadContentMessages,
hasUnreadOriginalContentMessages = roomSummaryEntity.hasUnreadOriginalContentMessages, hasUnreadOriginalContentMessages = roomSummaryEntity.hasUnreadOriginalContentMessages,
markedUnread = roomSummaryEntity.markedUnread,
tags = tags, tags = tags,
typingUsers = typingUsers, typingUsers = typingUsers,
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,

View file

@ -45,6 +45,7 @@ internal open class RoomSummaryEntity(
var hasUnreadMessages: Boolean = false, var hasUnreadMessages: Boolean = false,
var hasUnreadContentMessages: Boolean = false, var hasUnreadContentMessages: Boolean = false,
var hasUnreadOriginalContentMessages: Boolean = false, var hasUnreadOriginalContentMessages: Boolean = false,
var markedUnread: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null, var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS, var breadcrumbsIndex: Int = RoomSummary.NOT_IN_BREADCRUMBS,

View file

@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.database.model.ReadReceiptEntity
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
import io.realm.Realm import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity
internal fun isEventRead(realmConfiguration: RealmConfiguration, internal fun isEventRead(realmConfiguration: RealmConfiguration,
userId: String?, 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
}
}

View file

@ -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.DefaultInviteThreePidTask
import org.matrix.android.sdk.internal.session.room.membership.threepid.InviteThreePidTask 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.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.DefaultSetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask 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.read.SetReadMarkersTask
import org.matrix.android.sdk.internal.session.room.relation.DefaultFetchEditHistoryTask import org.matrix.android.sdk.internal.session.room.relation.DefaultFetchEditHistoryTask
import org.matrix.android.sdk.internal.session.room.relation.DefaultFindReactionEventForUndoTask import org.matrix.android.sdk.internal.session.room.relation.DefaultFindReactionEventForUndoTask
@ -154,6 +156,9 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask abstract fun bindMarkAllRoomsReadTask(task: DefaultMarkAllRoomsReadTask): MarkAllRoomsReadTask
@Binds
abstract fun bindSetMarkedUnreadTask(task: DefaultSetMarkedUnreadTask): SetMarkedUnreadTask
@Binds @Binds
abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask abstract fun bindFindReactionEventForUndoTask(task: DefaultFindReactionEventForUndoTask): FindReactionEventForUndoTask

View file

@ -42,6 +42,7 @@ internal class DefaultReadService @AssistedInject constructor(
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val taskExecutor: TaskExecutor, private val taskExecutor: TaskExecutor,
private val setReadMarkersTask: SetReadMarkersTask, private val setReadMarkersTask: SetReadMarkersTask,
private val setMarkedUnreadTask: SetMarkedUnreadTask,
private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper, private val readReceiptsSummaryMapper: ReadReceiptsSummaryMapper,
@UserId private val userId: String @UserId private val userId: String
) : ReadService { ) : ReadService {
@ -62,6 +63,8 @@ internal class DefaultReadService @AssistedInject constructor(
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
// Automatically unset unread marker
setMarkedUnreadFlag(false, callback)
} }
override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) { override fun setReadReceipt(eventId: String, callback: MatrixCallback<Unit>) {
@ -82,6 +85,25 @@ internal class DefaultReadService @AssistedInject constructor(
.executeBy(taskExecutor) .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 { override fun isEventRead(eventId: String): Boolean {
return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId) return isEventRead(monarchy.realmConfiguration, userId, roomId, eventId)
} }

View file

@ -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) { override suspend fun execute(params: MarkAllRoomsReadTask.Params) {
params.roomIds.forEach { roomId -> params.roomIds.forEach { roomId ->
readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true)) readMarkersTask.execute(SetReadMarkersTask.Params(roomId, forceReadMarker = true, forceReadReceipt = true))
} }
params.roomIds.forEach { roomId ->
markUnreadTask.execute(SetMarkedUnreadTask.Params(roomId, markedUnread = false))
}
} }
} }

View file

@ -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
)

View file

@ -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
}
}
}

View file

@ -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
}
}
}

View file

@ -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.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.RoomMemberEventHandler 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.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.summary.RoomSummaryUpdater
import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline import org.matrix.android.sdk.internal.session.room.timeline.DefaultTimeline
import org.matrix.android.sdk.internal.session.room.timeline.PaginationDirection 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 roomSummaryUpdater: RoomSummaryUpdater,
private val roomTagHandler: RoomTagHandler, private val roomTagHandler: RoomTagHandler,
private val roomFullyReadHandler: RoomFullyReadHandler, private val roomFullyReadHandler: RoomFullyReadHandler,
private val roomMarkedUnreadHandler: RoomMarkedUnreadHandler,
private val cryptoService: DefaultCryptoService, private val cryptoService: DefaultCryptoService,
private val roomMemberEventHandler: RoomMemberEventHandler, private val roomMemberEventHandler: RoomMemberEventHandler,
private val roomTypingUsersHandler: RoomTypingUsersHandler, private val roomTypingUsersHandler: RoomTypingUsersHandler,
@ -407,6 +409,9 @@ internal class RoomSyncHandler @Inject constructor(private val readReceiptHandle
} else if (eventType == EventType.FULLY_READ) { } else if (eventType == EventType.FULLY_READ) {
val content = event.getClearContent().toModel<FullyReadContent>() val content = event.getClearContent().toModel<FullyReadContent>()
roomFullyReadHandler.handle(realm, roomId, content) roomFullyReadHandler.handle(realm, roomId, content)
} else if (eventType == EventType.MARKED_UNREAD) {
val content = event.getClearContent().toModel<MarkedUnreadContent>()
roomMarkedUnreadHandler.handle(realm, roomId, content)
} }
} }
} }

View file

@ -35,4 +35,18 @@ interface AccountDataAPI {
fun setAccountData(@Path("userId") userId: String, fun setAccountData(@Path("userId") userId: String,
@Path("type") type: String, @Path("type") type: String,
@Body params: Any): Call<Unit> @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>
} }

View file

@ -63,8 +63,9 @@ class BreadcrumbsController @Inject constructor(
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
matrixItem(it.toMatrixItem()) matrixItem(it.toMatrixItem())
unreadNotificationCount(it.notificationCount) unreadNotificationCount(it.notificationCount)
markedUnread(it.markedUnread)
showHighlighted(it.highlightCount > 0) showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.scHasUnreadMessages(scSdkPreferences)) hasUnreadMessage(it.scIsUnread(scSdkPreferences))
hasDraft(it.userDrafts.isNotEmpty()) hasDraft(it.userDrafts.isNotEmpty())
itemClickListener( itemClickListener(
DebouncedClickListener(View.OnClickListener { _ -> DebouncedClickListener(View.OnClickListener { _ ->

View file

@ -37,6 +37,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
@EpoxyAttribute lateinit var matrixItem: MatrixItem @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var unreadMessages: Int = 0 // SC addition @EpoxyAttribute var unreadMessages: Int = 0 // SC addition
@EpoxyAttribute var markedUnread: Boolean = false
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false
@ -47,7 +48,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
holder.rootView.setOnClickListener(itemClickListener) holder.rootView.setOnClickListener(itemClickListener)
holder.unreadIndentIndicator.isVisible = hasUnreadMessage holder.unreadIndentIndicator.isVisible = hasUnreadMessage
avatarRenderer.render(matrixItem, holder.avatarImageView) 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.draftIndentIndicator.isVisible = hasDraft
holder.typingIndicator.isVisible = hasTypingUsers holder.typingIndicator.isVisible = hasTypingUsers
} }

View file

@ -1217,7 +1217,7 @@ class RoomDetailFragment @Inject constructor(
val inviter = state.asyncInviter() val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) { if (summary?.membership == Membership.JOIN) {
jumpToBottomView.count = summary.notificationCount jumpToBottomView.count = summary.notificationCount
jumpToBottomView.drawBadge = summary.scHasUnreadMessages(ScSdkPreferences(context)) jumpToBottomView.drawBadge = summary.scIsUnread(ScSdkPreferences(context))
scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline scrollOnHighlightedEventCallback.timeline = roomDetailViewModel.timeline
timelineEventController.update(state) timelineEventController.update(state)
inviteView.visibility = View.GONE inviteView.visibility = View.GONE

View file

@ -35,6 +35,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
@EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var unreadMessages: Int = 0 @EpoxyAttribute var unreadMessages: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var markedUnread: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
@ -44,7 +45,7 @@ abstract class RoomCategoryItem : VectorEpoxyModel<RoomCategoryItem.Holder>() {
val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also { val expandedArrowDrawable = ContextCompat.getDrawable(holder.rootView.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor) 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.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
holder.titleView.text = title holder.titleView.text = title
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }

View file

@ -29,5 +29,6 @@ sealed class RoomListAction : VectorViewModelAction {
data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction()
data class ToggleTag(val roomId: String, val tag: String) : RoomListAction() data class ToggleTag(val roomId: String, val tag: String) : RoomListAction()
data class LeaveRoom(val roomId: String) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction()
data class SetMarkedUnread(val roomId: String, val markedUnread: Boolean) : RoomListAction()
object MarkAllRoomsRead : RoomListAction() object MarkAllRoomsRead : RoomListAction()
} }

View file

@ -262,6 +262,12 @@ class RoomListFragment @Inject constructor(
is RoomListQuickActionsSharedAction.LowPriority -> { is RoomListQuickActionsSharedAction.LowPriority -> {
roomListViewModel.handle(RoomListAction.ToggleTag(quickAction.roomId, RoomTag.ROOM_TAG_LOW_PRIORITY)) 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 -> { is RoomListQuickActionsSharedAction.Leave -> {
AlertDialog.Builder(requireContext()) AlertDialog.Builder(requireContext())
.setTitle(R.string.room_participants_leave_prompt_title) .setTitle(R.string.room_participants_leave_prompt_title)

View file

@ -84,6 +84,7 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState,
is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.LeaveRoom -> handleLeaveRoom(action)
is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action)
is RoomListAction.ToggleTag -> handleToggleTag(action) is RoomListAction.ToggleTag -> handleToggleTag(action)
is RoomListAction.SetMarkedUnread -> handleSetMarkedUnread(action)
}.exhaustive }.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<Unit> {
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) { private fun handleLeaveRoom(action: RoomListAction.LeaveRoom) {
_viewEvents.post(RoomListViewEvents.Loading(null)) _viewEvents.post(RoomListViewEvents.Loading(null))
session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> { session.getRoom(action.roomId)?.leave(null, object : MatrixCallback<Unit> {

View file

@ -130,7 +130,7 @@ data class RoomListViewState(
get() = asyncFilteredRooms.invoke() get() = asyncFilteredRooms.invoke()
?.flatMap { it.value } ?.flatMap { it.value }
?.filter { it.membership == Membership.JOIN } ?.filter { it.membership == Membership.JOIN }
?.any { it.scHasUnreadMessages(scSdkPreferences) } ?.any { it.scIsUnread(scSdkPreferences) }
?: false ?: false
} }

View file

@ -134,14 +134,20 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
val unreadCount = if (summaries.isEmpty()) { val unreadCount = if (summaries.isEmpty()) {
0 0
} else { } 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 // SC addition
val unreadMessages = if (summaries.isEmpty() || !userPreferencesProvider.shouldShowUnimportantCounterBadge()) { val unreadMessages = if (summaries.isEmpty() || !userPreferencesProvider.shouldShowUnimportantCounterBadge()) {
0 0
} else { } else {
// TODO actual sum of events instead of sum of chats? // 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 } val showHighlighted = summaries.any { it.highlightCount > 0 }
roomCategoryItem { roomCategoryItem {
@ -151,6 +157,7 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri
unreadNotificationCount(unreadCount) unreadNotificationCount(unreadCount)
unreadMessages(unreadMessages) unreadMessages(unreadMessages)
showHighlighted(showHighlighted) showHighlighted(showHighlighted)
markedUnread(markedUnread)
listener { listener {
mutateExpandedState() mutateExpandedState()
update(viewState) update(viewState)

View file

@ -53,6 +53,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
@EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null @EpoxyAttribute var encryptionTrustLevel: RoomEncryptionTrustLevel? = null
@EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false
@EpoxyAttribute var markedUnread: Boolean = false
@EpoxyAttribute var hasDraft: Boolean = false @EpoxyAttribute var hasDraft: Boolean = false
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasFailedSending: Boolean = false @EpoxyAttribute var hasFailedSending: Boolean = false
@ -71,7 +72,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel<RoomSummaryItem.Holder>() {
holder.lastEventTimeView.text = lastEventTime holder.lastEventTimeView.text = lastEventTime
holder.lastEventView.text = lastFormattedEvent 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 // 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.unreadIndentIndicator.isVisible = hasUnreadMessage
holder.draftView.isVisible = hasDraft holder.draftView.isVisible = hasDraft
avatarRenderer.render(matrixItem, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)

View file

@ -105,7 +105,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
.showSelected(showSelected) .showSelected(showSelected)
.hasFailedSending(roomSummary.hasFailedSending) .hasFailedSending(roomSummary.hasFailedSending)
.unreadNotificationCount(unreadCount) .unreadNotificationCount(unreadCount)
.hasUnreadMessage(roomSummary.scHasUnreadMessages(scSdkPreferences)) .hasUnreadMessage(roomSummary.scIsUnread(scSdkPreferences))
.markedUnread(roomSummary.markedUnread)
.hasDraft(roomSummary.userDrafts.isNotEmpty()) .hasDraft(roomSummary.userDrafts.isNotEmpty())
.itemLongClickListener { _ -> .itemLongClickListener { _ ->
onLongClick?.invoke(roomSummary) ?: false onLongClick?.invoke(roomSummary) ?: false

View file

@ -30,11 +30,11 @@ class UnreadCounterBadgeView : AppCompatTextView {
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
fun render(state: State) { fun render(state: State) {
if (state.count == 0 && state.unread == 0) { if (state.count == 0 && state.unread == 0 && !state.markedUnread) {
visibility = View.INVISIBLE visibility = View.INVISIBLE
} else { } else {
visibility = View.VISIBLE visibility = View.VISIBLE
val bgRes = if (state.count > 0) { val bgRes = if (state.count > 0 || state.markedUnread) {
if (state.highlighted) { if (state.highlighted) {
R.drawable.bg_unread_highlight R.drawable.bg_unread_highlight
} else { } else {
@ -44,7 +44,12 @@ class UnreadCounterBadgeView : AppCompatTextView {
R.drawable.bg_unread_unimportant R.drawable.bg_unread_unimportant
} }
setBackgroundResource(bgRes) 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 count: Int,
val highlighted: Boolean, val highlighted: Boolean,
// SC addition // SC addition
val unread: Int val unread: Int,
val markedUnread: Boolean
) )
} }

View file

@ -22,6 +22,8 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetRoomPreviewItem
import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer 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.session.room.notification.RoomNotificationState
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
@ -31,7 +33,8 @@ import javax.inject.Inject
*/ */
class RoomListQuickActionsEpoxyController @Inject constructor( class RoomListQuickActionsEpoxyController @Inject constructor(
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val stringProvider: StringProvider private val stringProvider: StringProvider,
private val scSdkPreferences: ScSdkPreferences
) : TypedEpoxyController<RoomListQuickActionsState>() { ) : TypedEpoxyController<RoomListQuickActionsState>() {
var listener: Listener? = null var listener: Listener? = null
@ -54,6 +57,16 @@ class RoomListQuickActionsEpoxyController @Inject constructor(
lowPriorityClickListener { listener?.didSelectMenuAction(RoomListQuickActionsSharedAction.LowPriority(roomSummary.roomId)) } 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 // Notifications
dividerItem { dividerItem {
id("notifications_separator") id("notifications_separator")

View file

@ -27,6 +27,16 @@ sealed class RoomListQuickActionsSharedAction(
val destructive: Boolean = false) val destructive: Boolean = false)
: VectorSharedAction { : 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( data class NotificationsAllNoisy(val roomId: String) : RoomListQuickActionsSharedAction(
R.string.room_list_quick_actions_notifications_all_noisy, R.string.room_list_quick_actions_notifications_all_noisy,
R.drawable.ic_room_actions_notifications_all_noisy R.drawable.ic_room_actions_notifications_all_noisy

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" />
</vector>

View file

@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M23,12L20.56,9.22L20.9,5.54L17.29,4.72L15.4,1.54L12,3L8.6,1.54L6.71,4.72L3.1,5.53L3.44,9.21L1,12L3.44,14.78L3.1,18.47L6.71,19.29L8.6,22.47L12,21L15.4,22.46L17.29,19.28L20.9,18.46L20.56,14.78L23,12M13,17H11V15H13V17M13,13H11V7H13V13Z" />
</vector>

View file

@ -59,5 +59,7 @@
<string name="show_room_info_sc">Rauminfo</string> <string name="show_room_info_sc">Rauminfo</string>
<string name="show_participants_sc">Teilnehmer</string> <string name="show_participants_sc">Teilnehmer</string>
<string name="room_list_quick_actions_mark_room_unread">Als ungelesen markieren</string>
<string name="room_list_quick_actions_mark_room_read">Als gelesen markieren</string>
</resources> </resources>

View file

@ -59,5 +59,7 @@
<string name="show_room_info_sc">Room details</string> <string name="show_room_info_sc">Room details</string>
<string name="show_participants_sc">Participants</string> <string name="show_participants_sc">Participants</string>
<string name="room_list_quick_actions_mark_room_unread">Mark as unread</string>
<string name="room_list_quick_actions_mark_room_read">Mark as read</string>
</resources> </resources>