Skip to content
This repository was archived by the owner on May 23, 2025. It is now read-only.

Commit 01b3cb3

Browse files
author
Nik Clayton
authored
Fetch all outstanding Mastodon notifications when creating Android notifications (#3700)
* Fetch all outstanding Mastodon notifications when creating Android notifications Previous code fetched the oldest page of unfetched Mastodon notifications. If you had more than a page of Mastodon notifications you'd get Android notifications for that page, then ~ 15 minutes later Android notifications for the next page, and so on. This code fetches all the outstanding notifications at once. If this results in more than 40 total notifications the list is still trimmed so that a maximum of 40 Android notifications is displayed. Fixes #3648 * Build the list using buildList
1 parent 346dabf commit 01b3cb3

File tree

3 files changed

+56
-34
lines changed

3 files changed

+56
-34
lines changed

app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.keylesspalace.tusky.entity.Marker
1111
import com.keylesspalace.tusky.entity.Notification
1212
import com.keylesspalace.tusky.network.MastodonApi
1313
import com.keylesspalace.tusky.util.isLessThan
14+
import kotlinx.coroutines.runBlocking
1415
import javax.inject.Inject
1516
import kotlin.math.min
1617

@@ -35,10 +36,12 @@ class NotificationFetcher @Inject constructor(
3536
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
3637

3738
// Create sorted list of new notifications
38-
val notifications = fetchNewNotifications(account)
39-
.filter { filterNotification(notificationManager, account, it) }
40-
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
41-
.toMutableList()
39+
val notifications = runBlocking { // OK, because in a worker thread
40+
fetchNewNotifications(account)
41+
.filter { filterNotification(notificationManager, account, it) }
42+
.sortedWith(compareBy({ it.id.length }, { it.id })) // oldest notifications first
43+
.toMutableList()
44+
}
4245

4346
// There's a maximum limit on the number of notifications an Android app
4447
// can display. If the total number of notifications (current notifications,
@@ -114,7 +117,7 @@ class NotificationFetcher @Inject constructor(
114117
* ones that were last fetched here. So `lastNotificationId` takes precedence if it is greater
115118
* than the marker.
116119
*/
117-
private fun fetchNewNotifications(account: AccountEntity): List<Notification> {
120+
private suspend fun fetchNewNotifications(account: AccountEntity): List<Notification> {
118121
val authHeader = String.format("Bearer %s", account.accessToken)
119122

120123
// Figure out where to read from. Choose the most recent notification ID from:
@@ -128,21 +131,37 @@ class NotificationFetcher @Inject constructor(
128131
val markerId = if (remoteMarkerId.isLessThan(localMarkerId)) localMarkerId else remoteMarkerId
129132
val readingPosition = account.lastNotificationId
130133

131-
val minId = if (readingPosition.isLessThan(markerId)) markerId else readingPosition
134+
var minId: String? = if (readingPosition.isLessThan(markerId)) markerId else readingPosition
132135
Log.d(TAG, " remoteMarkerId: $remoteMarkerId")
133136
Log.d(TAG, " localMarkerId: $localMarkerId")
134137
Log.d(TAG, " readingPosition: $readingPosition")
135138

136139
Log.d(TAG, "getting Notifications for ${account.fullName}, min_id: $minId")
137140

138-
val notifications = mastodonApi.notificationsWithAuth(
139-
authHeader,
140-
account.domain,
141-
minId
142-
).blockingGet()
141+
// Fetch all outstanding notifications
142+
val notifications = buildList {
143+
while (minId != null) {
144+
val response = mastodonApi.notificationsWithAuth(
145+
authHeader,
146+
account.domain,
147+
minId = minId
148+
)
149+
if (!response.isSuccessful) break
150+
151+
// Notifications are returned in the page in order, newest first,
152+
// (https://github.com/mastodon/documentation/issues/1226), insert the
153+
// new page at the head of the list.
154+
response.body()?.let { addAll(0, it) }
155+
156+
// Get the previous page, which will be chronologically newer
157+
// notifications. If it doesn't exist this is null and the loop
158+
// will exit.
159+
val links = Links.from(response.headers()["link"])
160+
minId = links.prev
161+
}
162+
}
143163

144-
// Notifications are returned in order, most recent first. Save the newest notification ID
145-
// in the marker.
164+
// Save the newest notification ID in the marker.
146165
notifications.firstOrNull()?.let {
147166
val newMarkerId = notifications.first().id
148167
Log.d(TAG, "updating notification marker for ${account.fullName} to: $newMarkerId")
@@ -158,13 +177,13 @@ class NotificationFetcher @Inject constructor(
158177
return notifications
159178
}
160179

161-
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
180+
private suspend fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
162181
return try {
163182
val allMarkers = mastodonApi.markersWithAuth(
164183
authHeader,
165184
account.domain,
166185
listOf("notifications")
167-
).blockingGet()
186+
)
168187
val notificationMarker = allMarkers["notifications"]
169188
Log.d(TAG, "Fetched marker for ${account.fullName}: $notificationMarker")
170189
notificationMarker

app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingSource.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,21 @@ import retrofit2.Response
3131
import javax.inject.Inject
3232

3333
/** Models next/prev links from the "Links" header in an API response */
34-
data class Links(val next: String?, val prev: String?)
34+
data class Links(val next: String?, val prev: String?) {
35+
companion object {
36+
fun from(linkHeader: String?): Links {
37+
val links = HttpHeaderLink.parse(linkHeader)
38+
return Links(
39+
next = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter(
40+
"max_id"
41+
),
42+
prev = HttpHeaderLink.findByRelationType(links, "prev")?.uri?.getQueryParameter(
43+
"min_id"
44+
)
45+
)
46+
}
47+
}
48+
}
3549

3650
/** [PagingSource] for Mastodon Notifications, identified by the Notification ID */
3751
class NotificationsPagingSource @Inject constructor(
@@ -79,7 +93,7 @@ class NotificationsPagingSource @Inject constructor(
7993
return LoadResult.Error(Throwable("HTTP $code: $msg"))
8094
}
8195

82-
val links = getPageLinks(response.headers()["link"])
96+
val links = Links.from(response.headers()["link"])
8397
return LoadResult.Page(
8498
data = response.body()!!,
8599
nextKey = links.next,
@@ -188,18 +202,6 @@ class NotificationsPagingSource @Inject constructor(
188202
)
189203
}
190204

191-
private fun getPageLinks(linkHeader: String?): Links {
192-
val links = HttpHeaderLink.parse(linkHeader)
193-
return Links(
194-
next = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter(
195-
"max_id"
196-
),
197-
prev = HttpHeaderLink.findByRelationType(links, "prev")?.uri?.getQueryParameter(
198-
"min_id"
199-
)
200-
)
201-
}
202-
203205
override fun getRefreshKey(state: PagingState<String, Notification>): String? {
204206
return state.anchorPosition?.let { anchorPosition ->
205207
val anchorPage = state.closestPageToPosition(anchorPosition)

app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,27 +146,28 @@ interface MastodonApi {
146146
): Response<Notification>
147147

148148
@GET("api/v1/markers")
149-
fun markersWithAuth(
149+
suspend fun markersWithAuth(
150150
@Header("Authorization") auth: String,
151151
@Header(DOMAIN_HEADER) domain: String,
152152
@Query("timeline[]") timelines: List<String>
153-
): Single<Map<String, Marker>>
153+
): Map<String, Marker>
154154

155155
@FormUrlEncoded
156156
@POST("api/v1/markers")
157-
fun updateMarkersWithAuth(
157+
suspend fun updateMarkersWithAuth(
158158
@Header("Authorization") auth: String,
159159
@Header(DOMAIN_HEADER) domain: String,
160160
@Field("home[last_read_id]") homeLastReadId: String? = null,
161161
@Field("notifications[last_read_id]") notificationsLastReadId: String? = null
162162
): NetworkResult<Unit>
163163

164164
@GET("api/v1/notifications")
165-
fun notificationsWithAuth(
165+
suspend fun notificationsWithAuth(
166166
@Header("Authorization") auth: String,
167167
@Header(DOMAIN_HEADER) domain: String,
168+
/** Return results immediately newer than this ID */
168169
@Query("min_id") minId: String?
169-
): Single<List<Notification>>
170+
): Response<List<Notification>>
170171

171172
@POST("api/v1/notifications/clear")
172173
suspend fun clearNotifications(): Response<ResponseBody>

0 commit comments

Comments
 (0)