Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements MSC3773 (Thread Notifications) #7424

Merged
merged 16 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/7424.misc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Gets thread notifications from sync response
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ data class RoomSummary(
* Number of unread and highlighted message in this room.
*/
val highlightCount: Int = 0,
/**
* Number of threads with unread messages in this room.
*/
val threadNotificationCount: Int = 0,
/**
* Number of threads with highlighted messages in this room.
*/
val threadHighlightCount: Int = 0,
/**
* True if this room has unread messages.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ data class RoomSync(
*/
@Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null,

/**
* The count of threads with unread notifications (not the total # of notifications in all threads).
*/
@Json(name = "unread_thread_notifications") val unreadThreadNotifications: Map<String, RoomSyncUnreadThreadNotifications>? = null,

/**
* The room summary.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2022 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.api.session.sync.model

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

@JsonClass(generateAdapter = true)
data class RoomSyncUnreadThreadNotifications(
/**
* The number of threads with unread messages that match the push notification rules.
*/
@Json(name = "notification_count") val notificationCount: Int? = null,

/**
* The number of threads with highlighted unread messages (subset of notifications).
*/
@Json(name = "highlight_count") val highlightCount: Int? = null
)
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject
Expand All @@ -65,7 +66,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer
) : MatrixRealmMigration(
dbName = "Session",
schemaVersion = 40L,
schemaVersion = 41L,
) {
/**
* Forces all RealmSessionStoreMigration instances to be equal.
Expand Down Expand Up @@ -115,5 +116,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 38) MigrateSessionTo038(realm).perform()
if (oldVersion < 39) MigrateSessionTo039(realm).perform()
if (oldVersion < 40) MigrateSessionTo040(realm).perform()
if (oldVersion < 41) MigrateSessionTo041(realm).perform()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
highlightCount = roomSummaryEntity.highlightCount,
notificationCount = roomSummaryEntity.notificationCount,
threadHighlightCount = roomSummaryEntity.threadHighlightCount,
threadNotificationCount = roomSummaryEntity.threadNotificationCount,
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
tags = tags,
typingUsers = typingUsers,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright (c) 2022 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.database.migration

import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
import org.matrix.android.sdk.internal.util.database.RealmMigrator

internal class MigrateSessionTo041(realm: DynamicRealm) : RealmMigrator(realm, 41) {

override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("RoomSummaryEntity")
?.addField(RoomSummaryEntityFields.THREAD_HIGHLIGHT_COUNT, Int::class.java)
?.addField(RoomSummaryEntityFields.THREAD_NOTIFICATION_COUNT, Int::class.java)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ internal open class RoomSummaryEntity(
if (value != field) field = value
}

var threadNotificationCount: Int = 0
set(value) {
if (value != field) field = value
}

var threadHighlightCount: Int = 0
set(value) {
if (value != field) field = value
}

var readMarkerId: String? = null
set(value) {
if (value != field) field = value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
endGroup()
}
if (filters.filterUseless) {
not()
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
}
if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal object FilterFactory {
limit = numberOfEvents,
// senders = listOf(userId),
// relationSenders = userId?.let { listOf(it) },
relationTypes = listOf(RelationType.THREAD)
relationTypes = listOf(RelationType.THREAD),
)
}

Expand All @@ -37,7 +37,7 @@ internal object FilterFactory {
limit = numberOfEvents,
containsUrl = true,
types = listOf(EventType.MESSAGE),
lazyLoadMembers = true
lazyLoadMembers = true,
)
}

Expand All @@ -55,30 +55,23 @@ internal object FilterFactory {
}

fun createDefaultRoomFilter(): RoomEventFilter {
return RoomEventFilter(
lazyLoadMembers = true
)
return RoomEventFilter(lazyLoadMembers = true)
}

fun createElementRoomFilter(): RoomEventFilter {
return RoomEventFilter(
lazyLoadMembers = true
lazyLoadMembers = true,
// TODO Enable this for optimization
// types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
)
}

private fun createElementTimelineFilter(): RoomEventFilter? {
return null // RoomEventFilter().apply {
// TODO Enable this for optimization
// types = listOfSupportedEventTypes.toMutableList()
// }
return RoomEventFilter(enableUnreadThreadNotifications = true)
}

private fun createElementStateFilter(): RoomEventFilter {
return RoomEventFilter(
lazyLoadMembers = true
)
return RoomEventFilter(lazyLoadMembers = true)
}

// Get only managed types by Element
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.filter

import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import org.matrix.android.sdk.api.session.sync.model.RoomSync
import org.matrix.android.sdk.internal.di.MoshiProvider

/**
Expand Down Expand Up @@ -74,9 +75,15 @@ internal data class RoomEventFilter(
*/
@Json(name = "contains_url") val containsUrl: Boolean? = null,
/**
* If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false.
* If true, enables lazy-loading of membership events.
* See Lazy-loading room members for more information.
* Defaults to false.
*/
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null,
/**
* If true, this will opt-in for the server to return unread threads notifications in [RoomSync].
*/
@Json(name = "unread_thread_notifications") val enableUnreadThreadNotifications: Boolean? = null,
) {

fun toJSONString(): String {
Expand All @@ -92,6 +99,7 @@ internal data class RoomEventFilter(
rooms != null ||
notRooms != null ||
containsUrl != null ||
lazyLoadMembers != null)
lazyLoadMembers != null ||
enableUnreadThreadNotifications != null)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications
import org.matrix.android.sdk.internal.crypto.EventDecryptor
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
Expand Down Expand Up @@ -91,6 +92,7 @@ internal class RoomSummaryUpdater @Inject constructor(
membership: Membership? = null,
roomSummary: RoomSyncSummary? = null,
unreadNotifications: RoomSyncUnreadNotifications? = null,
unreadThreadNotifications: Map<String, RoomSyncUnreadThreadNotifications>? = null,
updateMembers: Boolean = false,
inviterId: String? = null,
aggregator: SyncResponsePostTreatmentAggregator? = null
Expand All @@ -111,6 +113,14 @@ internal class RoomSummaryUpdater @Inject constructor(
roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0
roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0

roomSummaryEntity.threadHighlightCount = unreadThreadNotifications
?.count { (it.value.highlightCount ?: 0) > 0 }
?: 0

roomSummaryEntity.threadNotificationCount = unreadThreadNotifications
?.count { (it.value.notificationCount ?: 0) > 0 }
?: 0

if (membership != null) {
roomSummaryEntity.membership = membership
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ internal class DefaultSyncTask @Inject constructor(
executeRequest(globalErrorReceiver) {
syncAPI.sync(
params = requestParams,
readTimeOut = readTimeOut
readTimeOut = readTimeOut,
)
}
}
Expand Down Expand Up @@ -178,7 +178,7 @@ internal class DefaultSyncTask @Inject constructor(
syncRequestStateTracker.setSyncRequestState(
SyncRequestState.IncrementalSyncParsing(
rooms = nbRooms,
toDevice = nbToDevice
toDevice = nbToDevice,
)
)
syncResponseHandler.handleResponse(syncResponse, token, null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,7 @@ internal class RoomSyncHandler @Inject constructor(
Membership.JOIN,
roomSync.summary,
roomSync.unreadNotifications,
roomSync.unreadThreadNotifications,
updateMembers = hasRoomMember,
aggregator = aggregator
)
Expand Down Expand Up @@ -372,7 +373,8 @@ internal class RoomSyncHandler @Inject constructor(
roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
roomTypingUsersHandler.handle(realm, roomId, null)
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications, aggregator = aggregator)
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary,
roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator)
return roomEntity
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail

import android.net.Uri
import androidx.annotation.IdRes
import androidx.lifecycle.asFlow
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
Expand Down Expand Up @@ -408,21 +409,40 @@ class TimelineViewModel @AssistedInject constructor(
*/
private fun observeLocalThreadNotifications() {
if (room == null) return
room.flow()
.liveLocalUnreadThreadList()
.execute {
val threadList = it.invoke()
val isUserMentioned = threadList?.firstOrNull { threadRootEvent ->
threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
}?.let { true } ?: false
val numberOfLocalUnreadThreads = threadList?.size ?: 0
copy(
threadNotificationBadgeState = ThreadNotificationBadgeState(
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
isUserMentioned = isUserMentioned
)
)
}
val threadNotificationsSupported = session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping if (room == null) return would simplify the rest of the code.

if (threadNotificationsSupported) {
room.getRoomSummaryLive()
.asFlow()
.onEach {
it.getOrNull()?.let {
setState {
copy(
threadNotificationBadgeState = ThreadNotificationBadgeState(
numberOfLocalUnreadThreads = it.threadNotificationCount + it.threadHighlightCount,
isUserMentioned = it.threadHighlightCount > 0,
)
)
}
}
}
.launchIn(viewModelScope)
} else {
room.flow()
.liveLocalUnreadThreadList()
.execute {
val threadList = it.invoke()
val isUserMentioned = threadList?.firstOrNull { threadRootEvent ->
threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
} != null
val numberOfLocalUnreadThreads = threadList?.size ?: 0
copy(
threadNotificationBadgeState = ThreadNotificationBadgeState(
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
isUserMentioned = isUserMentioned
)
)
}
}
}

override fun handle(action: RoomDetailAction) {
Expand Down