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:
parent
17b7abb537
commit
e4c10f1ca4
7 changed files with 150 additions and 65 deletions
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
@ -79,7 +81,11 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
||||||
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
||||||
AppDatabase.MIGRATION_22_23)
|
AppDatabase.MIGRATION_22_23)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun notifier(context: Context): Notifier = SystemNotifier(context)
|
||||||
|
|
||||||
}
|
}
|
15
app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
Normal file
15
app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt
Normal 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
|
||||||
|
)
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue