fix: make feed notifications robust against subscription changes

This commit is contained in:
Bnyro
2025-05-07 20:58:12 +02:00
parent 54cf71740d
commit c8c29ac64f
6 changed files with 38 additions and 36 deletions

View File

@ -140,11 +140,13 @@ object PreferenceKeys {
// Internally saved data / not a preference
const val ERROR_LOG = "error_log"
const val LAST_STREAM_VIDEO_ID = "last_stream_video_id"
const val LAST_WATCHED_FEED_TIME = "last_watched_feed_time"
const val AUTH_PREF_FILE = "auth"
const val IMAGE_PROXY_URL = "image_proxy_url"
const val SELECTED_CHANNEL_GROUP = "selected_channel_group"
const val SELECTED_DOWNLOAD_SORT_TYPE = "selected_download_sort_type"
const val LAST_SHOWN_INFO_MESSAGE_VERSION_CODE = "last_shown_info_message_version"
// use the helper methods at PreferenceHelper to access these
const val LAST_USER_SEEN_FEED_TIME = "last_watched_feed_time"
const val LAST_REFRESHED_FEED_TIME = "last_refreshed_feed_time"
}

View File

@ -96,24 +96,24 @@ object PreferenceHelper {
authSettings.edit { putString(PreferenceKeys.USERNAME, newValue) }
}
fun setLastSeenVideoId(videoId: String) {
putString(PreferenceKeys.LAST_STREAM_VIDEO_ID, videoId)
}
fun getLastSeenVideoId(): String {
return getString(PreferenceKeys.LAST_STREAM_VIDEO_ID, "")
}
fun updateLastFeedWatchedTime(time: Long) {
fun updateLastFeedWatchedTime(time: Long, seenByUser: Boolean) {
// only update the time if the time is newer
// this avoids cases, where the user last saw an older video, which had already been seen,
// causing all following video to be incorrectly marked as unseen again
if (getLastCheckedFeedTime() < time)
putLong(PreferenceKeys.LAST_WATCHED_FEED_TIME, time)
if (getLastCheckedFeedTime(false) < time)
putLong(PreferenceKeys.LAST_REFRESHED_FEED_TIME, time)
// this value holds the last time the user opened the subscriptions feed
// whereas [LAST_REFRESHED_FEED_TIME] considers the last time the feed was loaded,
// which could also be possible in the background (e.g. via notifications)
if (seenByUser && getLastCheckedFeedTime(true) < time)
putLong(PreferenceKeys.LAST_USER_SEEN_FEED_TIME, time)
}
fun getLastCheckedFeedTime(): Long {
return getLong(PreferenceKeys.LAST_WATCHED_FEED_TIME, 0)
fun getLastCheckedFeedTime(seenByUser: Boolean): Long {
val key =
if (seenByUser) PreferenceKeys.LAST_USER_SEEN_FEED_TIME else PreferenceKeys.LAST_REFRESHED_FEED_TIME
return getLong(key, 0)
}
fun saveErrorLog(log: String) {

View File

@ -37,7 +37,6 @@ import com.github.libretube.constants.IntentData
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.ActivityMainBinding
import com.github.libretube.enums.ImportFormat
import com.github.libretube.extensions.toID
import com.github.libretube.helpers.ImportHelper
import com.github.libretube.helpers.IntentHelper
import com.github.libretube.helpers.NavBarHelper
@ -240,9 +239,12 @@ class MainActivity : BaseActivity() {
subscriptionsViewModel.fetchSubscriptions(this)
subscriptionsViewModel.videoFeed.observe(this) { feed ->
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime(seenByUser = true)
val lastSeenVideoIndex = feed.orEmpty()
.indexOfFirst { PreferenceHelper.getLastSeenVideoId() == it.url?.toID() }
.filter { !it.isUpcoming }
.indexOfFirst { it.uploaded <= lastCheckedFeedTime }
if (lastSeenVideoIndex < 1) return@observe
binding.bottomNav.getOrCreateBadge(R.id.subscriptionsFragment).apply {
number = lastSeenVideoIndex
backgroundColor = ThemeHelper.getThemeColor(

View File

@ -140,13 +140,17 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
// any other feed updates are caused by manual refreshing and thus should reset the scroll
// position to zero
var alreadyShowedFeedOnce = false
viewModel.videoFeed.observe(viewLifecycleOwner) {
if (!viewModel.isCurrentTabSubChannels && it != null) {
viewModel.videoFeed.observe(viewLifecycleOwner) { feed ->
if (!viewModel.isCurrentTabSubChannels && feed != null) {
lifecycleScope.launch {
showFeed(!alreadyShowedFeedOnce)
}
alreadyShowedFeedOnce = true
}
feed?.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.updateLastFeedWatchedTime(it, true)
}
}
// restore the scroll position, same conditions as above
@ -384,7 +388,7 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
// add an "all caught up item"
if (selectedSortOrder == 0) {
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime()
val lastCheckedFeedTime = PreferenceHelper.getLastCheckedFeedTime(seenByUser = true)
val caughtUpIndex =
feed.indexOfFirst { it.uploaded <= lastCheckedFeedTime && !it.isUpcoming }
if (caughtUpIndex > 0 && !feed[caughtUpIndex - 1].isUpcoming) {
@ -404,10 +408,6 @@ class SubscriptionsFragment : DynamicLayoutManagerFragment(R.layout.fragment_sub
binding.toggleSubs.text = getString(R.string.subscriptions)
feed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.updateLastFeedWatchedTime(it)
}
binding.subRefresh.isRefreshing = false
feedAdapter.submitList(sortedFeed) {

View File

@ -11,7 +11,6 @@ import com.github.libretube.api.SubscriptionHelper
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.api.obj.Subscription
import com.github.libretube.extensions.TAG
import com.github.libretube.extensions.toID
import com.github.libretube.extensions.toastFromMainDispatcher
import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.repo.FeedProgress
@ -41,9 +40,8 @@ class SubscriptionsViewModel : ViewModel() {
return@launch
}
this@SubscriptionsViewModel.videoFeed.postValue(videoFeed)
if (videoFeed.isNotEmpty()) {
// save the last recent video to the prefs for the notification worker
PreferenceHelper.setLastSeenVideoId(videoFeed[0].url!!.toID())
videoFeed.firstOrNull { !it.isUpcoming }?.uploaded?.let {
PreferenceHelper.updateLastFeedWatchedTime(it, false)
}
}
}

View File

@ -83,18 +83,15 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
val videoFeed = try {
withContext(Dispatchers.IO) {
SubscriptionHelper.getFeed(forceRefresh = true)
}
}.filter { !it.isUpcoming }
} catch (e: Exception) {
return false
}
val lastUserSeenVideoId = PreferenceHelper.getLastSeenVideoId()
val mostRecentStreamId = videoFeed.firstOrNull()?.url?.toID() ?: return true
// save the latest streams that got notified about
PreferenceHelper.setLastSeenVideoId(mostRecentStreamId)
val lastFeedCheckMillis = PreferenceHelper.getLastCheckedFeedTime(seenByUser = false)
// first time notifications are enabled or no new video available
if (lastUserSeenVideoId.isEmpty() || lastUserSeenVideoId == mostRecentStreamId) return true
if (lastFeedCheckMillis == 0L || videoFeed.none { it.uploaded > lastFeedCheckMillis }) return true
val channelsToIgnore = PreferenceHelper.getIgnorableNotificationChannels()
val enableShortsNotification =
@ -102,7 +99,7 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
val channelGroups = videoFeed.asSequence()
// filter the new videos until the last seen video in the feed
.takeWhile { it.url!!.toID() != lastUserSeenVideoId }
.filter { it.uploaded > lastFeedCheckMillis }
// don't show notifications for shorts videos if not enabled
.filter { enableShortsNotification || !it.isShort }
// hide for notifications unsubscribed channels
@ -110,6 +107,9 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
// group the new streams by the uploader
.groupBy { it.uploaderUrl!!.toID() }
// update the last feed check time in order to not show the same notification again
PreferenceHelper.updateLastFeedWatchedTime(videoFeed.first().uploaded, seenByUser = false)
// return if the previous video didn't get found or all the channels have notifications disabled
if (channelGroups.isEmpty()) return true
@ -226,6 +226,6 @@ class NotificationWorker(appContext: Context, parameters: WorkerParameters) :
companion object {
private const val INTENT_FLAGS = Intent.FLAG_ACTIVITY_CLEAR_TOP or
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}