Improve notifications fetching (#1930)

* Improve notifications fetching

 - Only fetch notifications up to the latest fetched one
 - Use timeline markers to avoid showing already seen notifications

* Apply some of the suggestions
This commit is contained in:
Ivan Kupalov 2020-09-20 18:43:28 +02:00 committed by GitHub
parent 17b7abb537
commit e4c10f1ca4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 150 additions and 65 deletions

View File

@ -425,7 +425,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
} }
) )
if(addSearchButton) { if (addSearchButton) {
mainDrawer.addItemsAtPosition(4, mainDrawer.addItemsAtPosition(4,
primaryDrawerItem { primaryDrawerItem {
nameRes = R.string.action_search nameRes = R.string.action_search
@ -457,7 +457,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupTabs(selectNotificationTab: Boolean) { private fun setupTabs(selectNotificationTab: Boolean) {
val activeTabLayout = if(preferences.getString("mainNavPosition", "top") == "bottom") { val activeTabLayout = if (preferences.getString("mainNavPosition", "top") == "bottom") {
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize) val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin (composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
@ -621,10 +621,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
.transform( .transform(
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
) )
.into(object : CustomTarget<Drawable>(){ .into(object : CustomTarget<Drawable>() {
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
mainToolbar.navigationIcon = resource mainToolbar.navigationIcon = resource
} }
override fun onLoadCleared(placeholder: Drawable?) { override fun onLoadCleared(placeholder: Drawable?) {
mainToolbar.navigationIcon = placeholder mainToolbar.navigationIcon = placeholder
} }

View File

@ -0,0 +1,82 @@
package com.keylesspalace.tusky.components.notifications
import android.util.Log
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Marker
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isLessThan
import javax.inject.Inject
class NotificationFetcher @Inject constructor(
private val mastodonApi: MastodonApi,
private val accountManager: AccountManager,
private val notifier: Notifier
) {
fun fetchAndShow() {
for (account in accountManager.getAllAccountsOrderedByActive()) {
if (account.notificationsEnabled) {
try {
val notifications = fetchNotifications(account)
notifications.forEachIndexed { index, notification ->
notifier.show(notification, account, index == 0)
}
accountManager.saveAccount(account)
} catch (e: Exception) {
Log.w(TAG, "Error while fetching notifications", e)
}
}
}
}
private fun fetchNotifications(account: AccountEntity): MutableList<Notification> {
val authHeader = String.format("Bearer %s", account.accessToken)
// We fetch marker to not load/show notifications which user has already seen
val marker = fetchMarker(authHeader, account)
if (marker != null && account.lastNotificationId.isLessThan(marker.lastReadId)) {
account.lastNotificationId = marker.lastReadId
}
Log.d(TAG, "getting Notifications for " + account.fullName)
val notifications = mastodonApi.notificationsWithAuth(
authHeader,
account.domain,
account.lastNotificationId
).blockingGet()
val newId = account.lastNotificationId
var newestId = ""
val result = mutableListOf<Notification>()
for (notification in notifications.reversed()) {
val currentId = notification.id
if (newestId.isLessThan(currentId)) {
newestId = currentId
account.lastNotificationId = currentId
}
if (newId.isLessThan(currentId)) {
result.add(notification)
}
}
return result
}
private fun fetchMarker(authHeader: String, account: AccountEntity): Marker? {
return try {
val allMarkers = mastodonApi.markersWithAuth(
authHeader,
account.domain,
listOf("notifications")
).blockingGet()
val notificationMarker = allMarkers["notifications"]
Log.d(TAG, "Fetched marker: $notificationMarker")
notificationMarker
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch marker", e)
null
}
}
companion object {
const val TAG = "NotificationFetcher"
}
}

View File

@ -16,82 +16,35 @@
package com.keylesspalace.tusky.components.notifications package com.keylesspalace.tusky.components.notifications
import android.content.Context import android.content.Context
import android.util.Log
import androidx.work.ListenableWorker import androidx.work.ListenableWorker
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerFactory import androidx.work.WorkerFactory
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.isLessThan
import java.io.IOException
import javax.inject.Inject import javax.inject.Inject
class NotificationWorker( class NotificationWorker(
private val context: Context, context: Context,
params: WorkerParameters, params: WorkerParameters,
private val mastodonApi: MastodonApi, private val notificationsFetcher: NotificationFetcher
private val accountManager: AccountManager
) : Worker(context, params) { ) : Worker(context, params) {
override fun doWork(): Result { override fun doWork(): Result {
val accountList = accountManager.getAllAccountsOrderedByActive() notificationsFetcher.fetchAndShow()
for (account in accountList) {
if (account.notificationsEnabled) {
try {
Log.d(TAG, "getting Notifications for " + account.fullName)
val notificationsResponse = mastodonApi.notificationsWithAuth(
String.format("Bearer %s", account.accessToken),
account.domain
).execute()
val notifications = notificationsResponse.body()
if (notificationsResponse.isSuccessful && notifications != null) {
onNotificationsReceived(account, notifications)
} else {
Log.w(TAG, "error receiving notifications")
}
} catch (e: IOException) {
Log.w(TAG, "error receiving notifications", e)
}
}
}
return Result.success() return Result.success()
} }
private fun onNotificationsReceived(account: AccountEntity, notificationList: List<Notification>) {
val newId = account.lastNotificationId
var newestId = ""
var isFirstOfBatch = true
notificationList.reversed().forEach { notification ->
val currentId = notification.id
if (newestId.isLessThan(currentId)) {
newestId = currentId
}
if (newId.isLessThan(currentId)) {
NotificationHelper.make(context, notification, account, isFirstOfBatch)
isFirstOfBatch = false
}
}
account.lastNotificationId = newestId
accountManager.saveAccount(account)
}
companion object {
private const val TAG = "NotificationWorker"
}
} }
class NotificationWorkerFactory @Inject constructor( class NotificationWorkerFactory @Inject constructor(
val api: MastodonApi, private val notificationsFetcher: NotificationFetcher
val accountManager: AccountManager ) : WorkerFactory() {
): WorkerFactory() {
override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters): ListenableWorker? { override fun createWorker(
if(workerClassName == NotificationWorker::class.java.name) { appContext: Context,
return NotificationWorker(appContext, workerParameters, api, accountManager) workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
if (workerClassName == NotificationWorker::class.java.name) {
return NotificationWorker(appContext, workerParameters, notificationsFetcher)
} }
return null return null
} }

View File

@ -0,0 +1,20 @@
package com.keylesspalace.tusky.components.notifications
import android.content.Context
import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Notification
/**
* Shows notifications.
*/
interface Notifier {
fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean)
}
class SystemNotifier(
private val context: Context
) : Notifier {
override fun show(notification: Notification, account: AccountEntity, isFirstInBatch: Boolean) {
NotificationHelper.make(context, notification, account, isFirstInBatch)
}
}

View File

@ -25,6 +25,8 @@ import androidx.room.Room
import com.keylesspalace.tusky.TuskyApplication import com.keylesspalace.tusky.TuskyApplication
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.EventHubImpl import com.keylesspalace.tusky.appstore.EventHubImpl
import com.keylesspalace.tusky.components.notifications.Notifier
import com.keylesspalace.tusky.components.notifications.SystemNotifier
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.network.TimelineCases
@ -82,4 +84,8 @@ class AppModule {
.build() .build()
} }
@Provides
@Singleton
fun notifier(context: Context): Notifier = SystemNotifier(context)
} }

View File

@ -0,0 +1,15 @@
package com.keylesspalace.tusky.entity
import com.google.gson.annotations.SerializedName
import java.util.*
/**
* API type for saving the scroll position of a timeline.
*/
data class Marker(
@SerializedName("last_read_id")
val lastReadId: String,
val version: Int,
@SerializedName("updated_at")
val updatedAt: Date
)

View File

@ -99,11 +99,19 @@ interface MastodonApi {
@Query("exclude_types[]") excludes: Set<Notification.Type>? @Query("exclude_types[]") excludes: Set<Notification.Type>?
): Call<List<Notification>> ): Call<List<Notification>>
@GET("api/v1/markers")
fun markersWithAuth(
@Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String,
@Query("timeline[]") timelines: List<String>
): Single<Map<String, Marker>>
@GET("api/v1/notifications") @GET("api/v1/notifications")
fun notificationsWithAuth( fun notificationsWithAuth(
@Header("Authorization") auth: String, @Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String @Header(DOMAIN_HEADER) domain: String,
): Call<List<Notification>> @Query("since_id") sinceId: String?
): Single<List<Notification>>
@POST("api/v1/notifications/clear") @POST("api/v1/notifications/clear")
fun clearNotifications(): Call<ResponseBody> fun clearNotifications(): Call<ResponseBody>