Skip to content

Commit 29d3856

Browse files
authored
Merge pull request element-hq#7424 from vector-im/feature/eric/msc3773
Implements MSC3773 (Thread Notifications)
2 parents e41b1a6 + b34468b commit 29d3856

File tree

15 files changed

+160
-37
lines changed

15 files changed

+160
-37
lines changed

changelog.d/7424.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Gets thread notifications from sync response

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/RoomSummary.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ data class RoomSummary(
9797
* Number of unread and highlighted message in this room.
9898
*/
9999
val highlightCount: Int = 0,
100+
/**
101+
* Number of threads with unread messages in this room.
102+
*/
103+
val threadNotificationCount: Int = 0,
104+
/**
105+
* Number of threads with highlighted messages in this room.
106+
*/
107+
val threadHighlightCount: Int = 0,
100108
/**
101109
* True if this room has unread messages.
102110
*/

matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/sync/model/RoomSync.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ data class RoomSync(
4747
*/
4848
@Json(name = "unread_notifications") val unreadNotifications: RoomSyncUnreadNotifications? = null,
4949

50+
/**
51+
* The count of threads with unread notifications (not the total # of notifications in all threads).
52+
*/
53+
@Json(name = "unread_thread_notifications") val unreadThreadNotifications: Map<String, RoomSyncUnreadThreadNotifications>? = null,
54+
5055
/**
5156
* The room summary.
5257
*/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.api.session.sync.model
18+
19+
import com.squareup.moshi.Json
20+
import com.squareup.moshi.JsonClass
21+
22+
@JsonClass(generateAdapter = true)
23+
data class RoomSyncUnreadThreadNotifications(
24+
/**
25+
* The number of threads with unread messages that match the push notification rules.
26+
*/
27+
@Json(name = "notification_count") val notificationCount: Int? = null,
28+
29+
/**
30+
* The number of threads with highlighted unread messages (subset of notifications).
31+
*/
32+
@Json(name = "highlight_count") val highlightCount: Int? = null
33+
)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo037
5757
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo038
5858
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo039
5959
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo040
60+
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo041
6061
import org.matrix.android.sdk.internal.util.Normalizer
6162
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
6263
import javax.inject.Inject
@@ -65,7 +66,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
6566
private val normalizer: Normalizer
6667
) : MatrixRealmMigration(
6768
dbName = "Session",
68-
schemaVersion = 40L,
69+
schemaVersion = 41L,
6970
) {
7071
/**
7172
* Forces all RealmSessionStoreMigration instances to be equal.
@@ -115,5 +116,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
115116
if (oldVersion < 38) MigrateSessionTo038(realm).perform()
116117
if (oldVersion < 39) MigrateSessionTo039(realm).perform()
117118
if (oldVersion < 40) MigrateSessionTo040(realm).perform()
119+
if (oldVersion < 41) MigrateSessionTo041(realm).perform()
118120
}
119121
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ internal class RoomSummaryMapper @Inject constructor(
6161
otherMemberIds = roomSummaryEntity.otherMemberIds.toList(),
6262
highlightCount = roomSummaryEntity.highlightCount,
6363
notificationCount = roomSummaryEntity.notificationCount,
64+
threadHighlightCount = roomSummaryEntity.threadHighlightCount,
65+
threadNotificationCount = roomSummaryEntity.threadNotificationCount,
6466
hasUnreadMessages = roomSummaryEntity.hasUnreadMessages,
6567
tags = tags,
6668
typingUsers = typingUsers,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.database.migration
18+
19+
import io.realm.DynamicRealm
20+
import org.matrix.android.sdk.internal.database.model.RoomSummaryEntityFields
21+
import org.matrix.android.sdk.internal.util.database.RealmMigrator
22+
23+
internal class MigrateSessionTo041(realm: DynamicRealm) : RealmMigrator(realm, 41) {
24+
25+
override fun doMigrate(realm: DynamicRealm) {
26+
realm.schema.get("RoomSummaryEntity")
27+
?.addField(RoomSummaryEntityFields.THREAD_HIGHLIGHT_COUNT, Int::class.java)
28+
?.addField(RoomSummaryEntityFields.THREAD_NOTIFICATION_COUNT, Int::class.java)
29+
}
30+
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/RoomSummaryEntity.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ internal open class RoomSummaryEntity(
115115
if (value != field) field = value
116116
}
117117

118+
var threadNotificationCount: Int = 0
119+
set(value) {
120+
if (value != field) field = value
121+
}
122+
123+
var threadHighlightCount: Int = 0
124+
set(value) {
125+
if (value != field) field = value
126+
}
127+
118128
var readMarkerId: String? = null
119129
set(value) {
120130
if (value != field) field = value

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
110110
endGroup()
111111
}
112112
if (filters.filterUseless) {
113-
not()
114-
.equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
113+
not().equalTo(TimelineEventEntityFields.ROOT.IS_USELESS, true)
115114
}
116115
if (filters.filterEdits) {
117116
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/FilterFactory.kt

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal object FilterFactory {
2828
limit = numberOfEvents,
2929
// senders = listOf(userId),
3030
// relationSenders = userId?.let { listOf(it) },
31-
relationTypes = listOf(RelationType.THREAD)
31+
relationTypes = listOf(RelationType.THREAD),
3232
)
3333
}
3434

@@ -37,7 +37,7 @@ internal object FilterFactory {
3737
limit = numberOfEvents,
3838
containsUrl = true,
3939
types = listOf(EventType.MESSAGE),
40-
lazyLoadMembers = true
40+
lazyLoadMembers = true,
4141
)
4242
}
4343

@@ -55,30 +55,23 @@ internal object FilterFactory {
5555
}
5656

5757
fun createDefaultRoomFilter(): RoomEventFilter {
58-
return RoomEventFilter(
59-
lazyLoadMembers = true
60-
)
58+
return RoomEventFilter(lazyLoadMembers = true)
6159
}
6260

6361
fun createElementRoomFilter(): RoomEventFilter {
6462
return RoomEventFilter(
65-
lazyLoadMembers = true
63+
lazyLoadMembers = true,
6664
// TODO Enable this for optimization
6765
// types = (listOfSupportedEventTypes + listOfSupportedStateEventTypes).toMutableList()
6866
)
6967
}
7068

7169
private fun createElementTimelineFilter(): RoomEventFilter? {
72-
return null // RoomEventFilter().apply {
73-
// TODO Enable this for optimization
74-
// types = listOfSupportedEventTypes.toMutableList()
75-
// }
70+
return RoomEventFilter(enableUnreadThreadNotifications = true)
7671
}
7772

7873
private fun createElementStateFilter(): RoomEventFilter {
79-
return RoomEventFilter(
80-
lazyLoadMembers = true
81-
)
74+
return RoomEventFilter(lazyLoadMembers = true)
8275
}
8376

8477
// Get only managed types by Element

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/filter/RoomEventFilter.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package org.matrix.android.sdk.internal.session.filter
1717

1818
import com.squareup.moshi.Json
1919
import com.squareup.moshi.JsonClass
20+
import org.matrix.android.sdk.api.session.sync.model.RoomSync
2021
import org.matrix.android.sdk.internal.di.MoshiProvider
2122

2223
/**
@@ -74,9 +75,15 @@ internal data class RoomEventFilter(
7475
*/
7576
@Json(name = "contains_url") val containsUrl: Boolean? = null,
7677
/**
77-
* If true, enables lazy-loading of membership events. See Lazy-loading room members for more information. Defaults to false.
78+
* If true, enables lazy-loading of membership events.
79+
* See Lazy-loading room members for more information.
80+
* Defaults to false.
7881
*/
79-
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null
82+
@Json(name = "lazy_load_members") val lazyLoadMembers: Boolean? = null,
83+
/**
84+
* If true, this will opt-in for the server to return unread threads notifications in [RoomSync].
85+
*/
86+
@Json(name = "unread_thread_notifications") val enableUnreadThreadNotifications: Boolean? = null,
8087
) {
8188

8289
fun toJSONString(): String {
@@ -92,6 +99,7 @@ internal data class RoomEventFilter(
9299
rooms != null ||
93100
notRooms != null ||
94101
containsUrl != null ||
95-
lazyLoadMembers != null)
102+
lazyLoadMembers != null ||
103+
enableUnreadThreadNotifications != null)
96104
}
97105
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/summary/RoomSummaryUpdater.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
3939
import org.matrix.android.sdk.api.session.room.send.SendState
4040
import org.matrix.android.sdk.api.session.sync.model.RoomSyncSummary
4141
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadNotifications
42+
import org.matrix.android.sdk.api.session.sync.model.RoomSyncUnreadThreadNotifications
4243
import org.matrix.android.sdk.internal.crypto.EventDecryptor
4344
import org.matrix.android.sdk.internal.crypto.crosssigning.DefaultCrossSigningService
4445
import org.matrix.android.sdk.internal.database.mapper.ContentMapper
@@ -91,6 +92,7 @@ internal class RoomSummaryUpdater @Inject constructor(
9192
membership: Membership? = null,
9293
roomSummary: RoomSyncSummary? = null,
9394
unreadNotifications: RoomSyncUnreadNotifications? = null,
95+
unreadThreadNotifications: Map<String, RoomSyncUnreadThreadNotifications>? = null,
9496
updateMembers: Boolean = false,
9597
inviterId: String? = null,
9698
aggregator: SyncResponsePostTreatmentAggregator? = null
@@ -111,6 +113,14 @@ internal class RoomSummaryUpdater @Inject constructor(
111113
roomSummaryEntity.highlightCount = unreadNotifications?.highlightCount ?: 0
112114
roomSummaryEntity.notificationCount = unreadNotifications?.notificationCount ?: 0
113115

116+
roomSummaryEntity.threadHighlightCount = unreadThreadNotifications
117+
?.count { (it.value.highlightCount ?: 0) > 0 }
118+
?: 0
119+
120+
roomSummaryEntity.threadNotificationCount = unreadThreadNotifications
121+
?.count { (it.value.notificationCount ?: 0) > 0 }
122+
?: 0
123+
114124
if (membership != null) {
115125
roomSummaryEntity.membership = membership
116126
}

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/SyncTask.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ internal class DefaultSyncTask @Inject constructor(
140140
executeRequest(globalErrorReceiver) {
141141
syncAPI.sync(
142142
params = requestParams,
143-
readTimeOut = readTimeOut
143+
readTimeOut = readTimeOut,
144144
)
145145
}
146146
}
@@ -178,7 +178,7 @@ internal class DefaultSyncTask @Inject constructor(
178178
syncRequestStateTracker.setSyncRequestState(
179179
SyncRequestState.IncrementalSyncParsing(
180180
rooms = nbRooms,
181-
toDevice = nbToDevice
181+
toDevice = nbToDevice,
182182
)
183183
)
184184
syncResponseHandler.handleResponse(syncResponse, token, null)

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/sync/handler/room/RoomSyncHandler.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ internal class RoomSyncHandler @Inject constructor(
287287
Membership.JOIN,
288288
roomSync.summary,
289289
roomSync.unreadNotifications,
290+
roomSync.unreadThreadNotifications,
290291
updateMembers = hasRoomMember,
291292
aggregator = aggregator
292293
)
@@ -372,7 +373,8 @@ internal class RoomSyncHandler @Inject constructor(
372373
roomEntity.chunks.clearWith { it.deleteOnCascade(deleteStateEvents = true, canDeleteRoot = true) }
373374
roomTypingUsersHandler.handle(realm, roomId, null)
374375
roomChangeMembershipStateDataSource.setMembershipFromSync(roomId, Membership.LEAVE)
375-
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary, roomSync.unreadNotifications, aggregator = aggregator)
376+
roomSummaryUpdater.update(realm, roomId, membership, roomSync.summary,
377+
roomSync.unreadNotifications, roomSync.unreadThreadNotifications, aggregator = aggregator)
376378
return roomEntity
377379
}
378380

vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail
1818

1919
import android.net.Uri
2020
import androidx.annotation.IdRes
21+
import androidx.lifecycle.asFlow
2122
import com.airbnb.mvrx.Async
2223
import com.airbnb.mvrx.Fail
2324
import com.airbnb.mvrx.Loading
@@ -408,21 +409,40 @@ class TimelineViewModel @AssistedInject constructor(
408409
*/
409410
private fun observeLocalThreadNotifications() {
410411
if (room == null) return
411-
room.flow()
412-
.liveLocalUnreadThreadList()
413-
.execute {
414-
val threadList = it.invoke()
415-
val isUserMentioned = threadList?.firstOrNull { threadRootEvent ->
416-
threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
417-
}?.let { true } ?: false
418-
val numberOfLocalUnreadThreads = threadList?.size ?: 0
419-
copy(
420-
threadNotificationBadgeState = ThreadNotificationBadgeState(
421-
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
422-
isUserMentioned = isUserMentioned
423-
)
424-
)
425-
}
412+
val threadNotificationsSupported = session.homeServerCapabilitiesService().getHomeServerCapabilities().canUseThreadReadReceiptsAndNotifications
413+
if (threadNotificationsSupported) {
414+
room.getRoomSummaryLive()
415+
.asFlow()
416+
.onEach {
417+
it.getOrNull()?.let {
418+
setState {
419+
copy(
420+
threadNotificationBadgeState = ThreadNotificationBadgeState(
421+
numberOfLocalUnreadThreads = it.threadNotificationCount + it.threadHighlightCount,
422+
isUserMentioned = it.threadHighlightCount > 0,
423+
)
424+
)
425+
}
426+
}
427+
}
428+
.launchIn(viewModelScope)
429+
} else {
430+
room.flow()
431+
.liveLocalUnreadThreadList()
432+
.execute {
433+
val threadList = it.invoke()
434+
val isUserMentioned = threadList?.firstOrNull { threadRootEvent ->
435+
threadRootEvent.root.threadDetails?.threadNotificationState == ThreadNotificationState.NEW_HIGHLIGHTED_MESSAGE
436+
} != null
437+
val numberOfLocalUnreadThreads = threadList?.size ?: 0
438+
copy(
439+
threadNotificationBadgeState = ThreadNotificationBadgeState(
440+
numberOfLocalUnreadThreads = numberOfLocalUnreadThreads,
441+
isUserMentioned = isUserMentioned
442+
)
443+
)
444+
}
445+
}
426446
}
427447

428448
override fun handle(action: RoomDetailAction) {

0 commit comments

Comments
 (0)